diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 151cab9d7..109a03fc4 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -474,7 +474,7 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO const svg_ptr = chain.get(i); svg_ptr.* = .{ ._tag_name = try String.init(self._arena, tag_name, .{}), - ._type = unionInit(Element.Svg.Type, chain.get(i + 1)), + ._type = typeInit(Element.Svg, chain.get(i + 1)), }; setProto(svg_ptr, chain.get(i - 1)); } else { diff --git a/src/browser/dump.zig b/src/browser/dump.zig index c2db77472..89c35423b 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -261,7 +261,7 @@ fn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, frame: *Fram fn isVoidElement(el: *const Node.Element) bool { return switch (el._type) { - .html => |html| switch (html._type) { + .html => switch (el.subtype(Node.Element.Html)._type) { .br, .hr, .img, .input, .link, .meta => true, else => false, }, diff --git a/src/browser/frame/user_input.zig b/src/browser/frame/user_input.zig index 385919b5b..153d8c0db 100644 --- a/src/browser/frame/user_input.zig +++ b/src/browser/frame/user_input.zig @@ -208,7 +208,7 @@ fn hasClickActivationBehavior(node: *Node) bool { return switch (html_element._type) { .anchor => element.getAttributeSafe(comptime .wrap("href")) != null, .input, .button, .select, .textarea, .label => true, - .generic => |generic| generic._tag == .summary, + .generic => html_element.subtype(Element.Html.Generic)._tag == .summary, else => false, }; } @@ -328,7 +328,8 @@ pub fn handleClick(frame: *Frame, target: *Node) !void { const html_element = element.is(Element.Html) orelse return; switch (html_element._type) { - .anchor => |anchor| { + .anchor => { + const anchor = html_element.subtype(Element.Html.Anchor); const href = element.getAttributeSafe(comptime .wrap("href")) orelse return; if (href.len == 0) { return; @@ -363,7 +364,8 @@ pub fn handleClick(frame: *Frame, target: *Node) !void { .kind = .{ .push = null }, }, .{ .anchor = target_frame }); }, - .input => |input| { + .input => { + const input = html_element.subtype(Element.Html.Input); try element.focus(frame); // Per HTML §4.10.18.6.4 "Image Button state (type=image)", clicking an // image button submits its form. The form-data set already gets the @@ -373,14 +375,16 @@ pub fn handleClick(frame: *Frame, target: *Node) !void { return frame.submitForm(element, input.getForm(frame), .{}); } }, - .button => |button| { + .button => { + const button = html_element.subtype(Element.Html.Button); try element.focus(frame); if (std.mem.eql(u8, button.getType(), "submit")) { return frame.submitForm(element, button.getForm(frame), .{}); } }, .select, .textarea => try element.focus(frame), - .label => |label| { + .label => { + const label = html_element.subtype(Element.Html.Label); // Per HTML §4.10.4 "The label element", a label's activation // behavior is to run the synthetic click activation steps on the // labeled control. Mirrors Chrome's HTMLLabelElement::DefaultEventHandler. @@ -388,8 +392,8 @@ pub fn handleClick(frame: *Frame, target: *Node) !void { const control_html = control.is(Element.Html) orelse return; try control_html.click(frame); }, - .generic => |generic| { - switch (generic._tag) { + .generic => { + switch (html_element.subtype(Element.Html.Generic)._tag) { .summary => { const parent_el = target.parentElement() orelse return; const details = parent_el.is(Element.Html.Details) orelse return; diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index e84766246..0d65665c2 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -1267,7 +1267,15 @@ pub fn resolveValue(value: anytype) Resolved { // (e.g. CData); the type maps the tag to the member's type. if (comptime @typeInfo(@TypeOf(value._type)) == .@"enum" and @hasDecl(T, "Subtype")) { switch (value._type) { - inline else => |tag| return resolveValue(value.subtype(T.Subtype(tag))), + inline else => |tag| { + const S = T.Subtype(tag); + if (S == T) { + // A tag can map to the type itself (e.g. Media.generic); + // the value is already the most specific type. + return resolveT(T, value); + } + return resolveValue(value.subtype(S)); + }, } } diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index 1de032884..c8d9ac364 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -600,7 +600,7 @@ fn getTemplateContentsCallback(ctx: *anyopaque, target_ref: *anyopaque) callconv fn _getTemplateContentsCallback(self: *Parser, node: *Node) !*anyopaque { const element = node.as(Element); - const template = element._type.html.is(Element.Html.Template) orelse unreachable; + const template = element.subtype(Element.Html).is(Element.Html.Template) orelse unreachable; const content_node = template.getContent().asNode(); // Create a ParsedNode wrapper for the content DocumentFragment diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 180addb36..4cdf07cc2 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -115,15 +115,35 @@ _attributes: Attribute.List = .{}, // work to resolve the proto). _proto_canary: if (lp.IS_DEBUG) *Node else void = undefined, -pub const Type = union(enum) { - html: *Html, - svg: *Svg, +pub const Type = enum(u8) { + html, + svg, }; +pub fn Subtype(comptime tag: Type) type { + return switch (tag) { + .html => Html, + .svg => Svg, + }; +} + +pub fn subtype(self: *const Element, comptime T: type) *T { + const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, Element); + const sub: *T = @ptrFromInt(@intFromPtr(self) + offset); + if (comptime lp.IS_DEBUG) { + // This pointer dance only works because the factory allocates the chain + // in a contiguous block of memory. In debug, we assert this holds via + // the _proto_canary back pointer. + std.debug.assert(Factory.protoOf(sub) == self); + } + return sub; +} + pub fn is(self: *Element, comptime T: type) ?*T { const type_name = @typeName(T); switch (self._type) { - .html => |el| { + .html => { + const el = self.subtype(Html); if (T == Html) { return el; } @@ -131,7 +151,8 @@ pub fn is(self: *Element, comptime T: type) ?*T { return el.is(T); } }, - .svg => |svg| { + .svg => { + const svg = self.subtype(Svg); if (T == Svg) { return svg; } @@ -178,177 +199,183 @@ pub fn isEqualNode(self: *Element, other: *Element) bool { pub fn getTagNameLower(self: *const Element) []const u8 { switch (self._type) { - .html => |he| switch (he._type) { - .custom => |ce| { - @branchHint(.unlikely); - return ce._tag_name.str(); - }, - else => return switch (he._type) { - .anchor => "a", - .area => "area", - .base => "base", - .body => "body", - .br => "br", - .button => "button", - .canvas => "canvas", - .custom => |e| e._tag_name.str(), - .data => "data", - .datalist => "datalist", - .details => "details", - .dialog => "dialog", - .directory => "dir", - .div => "div", - .dl => "dl", - .embed => "embed", - .fieldset => "fieldset", - .font => "font", - .frameset => "frameset", - .form => "form", - .generic => |e| e._tag_name.str(), - .heading => |e| e._tag_name.str(), - .head => "head", - .html => "html", - .hr => "hr", - .iframe => "iframe", - .img => "img", - .input => "input", - .label => "label", - .legend => "legend", - .li => "li", - .link => "link", - .map => "map", - .marquee => "marquee", - .media => |m| switch (m._type) { - .audio => "audio", - .video => "video", - .generic => "media", + .html => { + const he = self.subtype(Html); + switch (he._type) { + .custom => { + @branchHint(.unlikely); + return he.subtype(Html.Custom)._tag_name.str(); }, - .meta => "meta", - .meter => "meter", - .mod => |e| e._tag_name.str(), - .object => "object", - .ol => "ol", - .optgroup => "optgroup", - .option => "option", - .output => "output", - .p => "p", - .picture => "picture", - .param => "param", - .pre => "pre", - .progress => "progress", - .quote => |e| e._tag_name.str(), - .script => "script", - .select => "select", - .slot => "slot", - .source => "source", - .span => "span", - .style => "style", - .table => "table", - .table_caption => "caption", - .table_cell => |e| e._tag_name.str(), - .table_col => |e| e._tag_name.str(), - .table_row => "tr", - .table_section => |e| e._tag_name.str(), - .template => "template", - .textarea => "textarea", - .time => "time", - .title => "title", - .track => "track", - .ul => "ul", - .unknown => |e| e._tag_name.str(), - }, + else => return switch (he._type) { + .anchor => "a", + .area => "area", + .base => "base", + .body => "body", + .br => "br", + .button => "button", + .canvas => "canvas", + .custom => he.subtype(Html.Custom)._tag_name.str(), + .data => "data", + .datalist => "datalist", + .details => "details", + .dialog => "dialog", + .directory => "dir", + .div => "div", + .dl => "dl", + .embed => "embed", + .fieldset => "fieldset", + .font => "font", + .frameset => "frameset", + .form => "form", + .generic => he.subtype(Html.Generic)._tag_name.str(), + .heading => he.subtype(Html.Heading)._tag_name.str(), + .head => "head", + .html => "html", + .hr => "hr", + .iframe => "iframe", + .img => "img", + .input => "input", + .label => "label", + .legend => "legend", + .li => "li", + .link => "link", + .map => "map", + .marquee => "marquee", + .media => switch (he.subtype(Html.Media)._type) { + .audio => "audio", + .video => "video", + .generic => "media", + }, + .meta => "meta", + .meter => "meter", + .mod => he.subtype(Html.Mod)._tag_name.str(), + .object => "object", + .ol => "ol", + .optgroup => "optgroup", + .option => "option", + .output => "output", + .p => "p", + .picture => "picture", + .param => "param", + .pre => "pre", + .progress => "progress", + .quote => he.subtype(Html.Quote)._tag_name.str(), + .script => "script", + .select => "select", + .slot => "slot", + .source => "source", + .span => "span", + .style => "style", + .table => "table", + .table_caption => "caption", + .table_cell => he.subtype(Html.TableCell)._tag_name.str(), + .table_col => he.subtype(Html.TableCol)._tag_name.str(), + .table_row => "tr", + .table_section => he.subtype(Html.TableSection)._tag_name.str(), + .template => "template", + .textarea => "textarea", + .time => "time", + .title => "title", + .track => "track", + .ul => "ul", + .unknown => he.subtype(Html.Unknown)._tag_name.str(), + }, + } }, - .svg => |svg| return svg._tag_name.str(), + .svg => return self.subtype(Svg)._tag_name.str(), } } pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 { return switch (self._type) { - .html => |he| switch (he._type) { - .anchor => "A", - .area => "AREA", - .base => "BASE", - .body => "BODY", - .br => "BR", - .button => "BUTTON", - .canvas => "CANVAS", - .custom => |e| upperTagName(&e._tag_name, buf), - .data => "DATA", - .datalist => "DATALIST", - .details => "DETAILS", - .dialog => "DIALOG", - .directory => "DIR", - .div => "DIV", - .dl => "DL", - .embed => "EMBED", - .fieldset => "FIELDSET", - .font => "FONT", - .frameset => "FRAMESET", - .form => "FORM", - .generic => |e| upperTagName(&e._tag_name, buf), - .heading => |e| upperTagName(&e._tag_name, buf), - .head => "HEAD", - .html => "HTML", - .hr => "HR", - .iframe => "IFRAME", - .img => "IMG", - .input => "INPUT", - .label => "LABEL", - .legend => "LEGEND", - .li => "LI", - .link => "LINK", - .map => "MAP", - .marquee => "MARQUEE", - .meta => "META", - .media => |m| switch (m._type) { - .audio => "AUDIO", - .video => "VIDEO", - .generic => "MEDIA", - }, - .meter => "METER", - .mod => |e| upperTagName(&e._tag_name, buf), - .object => "OBJECT", - .ol => "OL", - .optgroup => "OPTGROUP", - .option => "OPTION", - .output => "OUTPUT", - .p => "P", - .picture => "PICTURE", - .param => "PARAM", - .pre => "PRE", - .progress => "PROGRESS", - .quote => |e| upperTagName(&e._tag_name, buf), - .script => "SCRIPT", - .select => "SELECT", - .slot => "SLOT", - .source => "SOURCE", - .span => "SPAN", - .style => "STYLE", - .table => "TABLE", - .table_caption => "CAPTION", - .table_cell => |e| upperTagName(&e._tag_name, buf), - .table_col => |e| upperTagName(&e._tag_name, buf), - .table_row => "TR", - .table_section => |e| upperTagName(&e._tag_name, buf), - .template => "TEMPLATE", - .textarea => "TEXTAREA", - .time => "TIME", - .title => "TITLE", - .track => "TRACK", - .ul => "UL", - .unknown => |e| switch (self._namespace) { - .html => upperTagName(&e._tag_name, buf), - .svg, .xml, .mathml, .unknown, .null => e._tag_name.str(), - }, + .html => blk: { + const he = self.subtype(Html); + break :blk switch (he._type) { + .anchor => "A", + .area => "AREA", + .base => "BASE", + .body => "BODY", + .br => "BR", + .button => "BUTTON", + .canvas => "CANVAS", + .custom => upperTagName(&he.subtype(Html.Custom)._tag_name, buf), + .data => "DATA", + .datalist => "DATALIST", + .details => "DETAILS", + .dialog => "DIALOG", + .directory => "DIR", + .div => "DIV", + .dl => "DL", + .embed => "EMBED", + .fieldset => "FIELDSET", + .font => "FONT", + .frameset => "FRAMESET", + .form => "FORM", + .generic => upperTagName(&he.subtype(Html.Generic)._tag_name, buf), + .heading => upperTagName(&he.subtype(Html.Heading)._tag_name, buf), + .head => "HEAD", + .html => "HTML", + .hr => "HR", + .iframe => "IFRAME", + .img => "IMG", + .input => "INPUT", + .label => "LABEL", + .legend => "LEGEND", + .li => "LI", + .link => "LINK", + .map => "MAP", + .marquee => "MARQUEE", + .meta => "META", + .media => switch (he.subtype(Html.Media)._type) { + .audio => "AUDIO", + .video => "VIDEO", + .generic => "MEDIA", + }, + .meter => "METER", + .mod => upperTagName(&he.subtype(Html.Mod)._tag_name, buf), + .object => "OBJECT", + .ol => "OL", + .optgroup => "OPTGROUP", + .option => "OPTION", + .output => "OUTPUT", + .p => "P", + .picture => "PICTURE", + .param => "PARAM", + .pre => "PRE", + .progress => "PROGRESS", + .quote => upperTagName(&he.subtype(Html.Quote)._tag_name, buf), + .script => "SCRIPT", + .select => "SELECT", + .slot => "SLOT", + .source => "SOURCE", + .span => "SPAN", + .style => "STYLE", + .table => "TABLE", + .table_caption => "CAPTION", + .table_cell => upperTagName(&he.subtype(Html.TableCell)._tag_name, buf), + .table_col => upperTagName(&he.subtype(Html.TableCol)._tag_name, buf), + .table_row => "TR", + .table_section => upperTagName(&he.subtype(Html.TableSection)._tag_name, buf), + .template => "TEMPLATE", + .textarea => "TEXTAREA", + .time => "TIME", + .title => "TITLE", + .track => "TRACK", + .ul => "UL", + .unknown => switch (self._namespace) { + .html => upperTagName(&he.subtype(Html.Unknown)._tag_name, buf), + .svg, .xml, .mathml, .unknown, .null => he.subtype(Html.Unknown)._tag_name.str(), + }, + }; }, - .svg => |svg| svg._tag_name.str(), + .svg => self.subtype(Svg)._tag_name.str(), }; } pub fn getTagNameDump(self: *const Element) []const u8 { switch (self._type) { .html => return self.getTagNameLower(), - .svg => |svg| return svg._tag_name.str(), + .svg => return self.subtype(Svg)._tag_name.str(), } } @@ -1916,81 +1943,84 @@ fn upperTagName(tag_name: *String, buf: []u8) []const u8 { pub fn getTag(self: *const Element) Tag { return switch (self._type) { - .html => |he| switch (he._type) { - .anchor => .anchor, - .area => .area, - .base => .base, - .div => .div, - .dl => .dl, - .embed => .embed, - .form => .form, - .p => .p, - .custom => .custom, - .data => .data, - .datalist => .datalist, - .details => .details, - .dialog => .dialog, - .directory => .directory, - .iframe => .iframe, - .img => .img, - .br => .br, - .button => .button, - .canvas => .canvas, - .fieldset => .fieldset, - .font => .font, - .frameset => .frameset, - .heading => |h| h._tag, - .label => .label, - .legend => .legend, - .li => .li, - .map => .map, - .marquee => .marquee, - .ul => .ul, - .ol => .ol, - .object => .object, - .optgroup => .optgroup, - .output => .output, - .picture => .picture, - .param => .param, - .pre => .pre, - .generic => |g| g._tag, - .media => |m| switch (m._type) { - .audio => .audio, - .video => .video, - .generic => .media, - }, - .meter => .meter, - .mod => |m| m._tag, - .progress => .progress, - .quote => |q| q._tag, - .script => .script, - .select => .select, - .slot => .slot, - .source => .source, - .span => .span, - .option => .option, - .table => .table, - .table_caption => .caption, - .table_cell => |tc| tc._tag, - .table_col => |tc| tc._tag, - .table_row => .tr, - .table_section => |ts| ts._tag, - .template => .template, - .textarea => .textarea, - .time => .time, - .track => .track, - .input => .input, - .link => .link, - .meta => .meta, - .hr => .hr, - .style => .style, - .title => .title, - .body => .body, - .html => .html, - .head => .head, - .unknown => .unknown, + .html => blk: { + const he = self.subtype(Html); + break :blk switch (he._type) { + .anchor => .anchor, + .area => .area, + .base => .base, + .div => .div, + .dl => .dl, + .embed => .embed, + .form => .form, + .p => .p, + .custom => .custom, + .data => .data, + .datalist => .datalist, + .details => .details, + .dialog => .dialog, + .directory => .directory, + .iframe => .iframe, + .img => .img, + .br => .br, + .button => .button, + .canvas => .canvas, + .fieldset => .fieldset, + .font => .font, + .frameset => .frameset, + .heading => he.subtype(Html.Heading)._tag, + .label => .label, + .legend => .legend, + .li => .li, + .map => .map, + .marquee => .marquee, + .ul => .ul, + .ol => .ol, + .object => .object, + .optgroup => .optgroup, + .output => .output, + .picture => .picture, + .param => .param, + .pre => .pre, + .generic => he.subtype(Html.Generic)._tag, + .media => switch (he.subtype(Html.Media)._type) { + .audio => .audio, + .video => .video, + .generic => .media, + }, + .meter => .meter, + .mod => he.subtype(Html.Mod)._tag, + .progress => .progress, + .quote => he.subtype(Html.Quote)._tag, + .script => .script, + .select => .select, + .slot => .slot, + .source => .source, + .span => .span, + .option => .option, + .table => .table, + .table_caption => .caption, + .table_cell => he.subtype(Html.TableCell)._tag, + .table_col => he.subtype(Html.TableCol)._tag, + .table_row => .tr, + .table_section => he.subtype(Html.TableSection)._tag, + .template => .template, + .textarea => .textarea, + .time => .time, + .track => .track, + .input => .input, + .link => .link, + .meta => .meta, + .hr => .hr, + .style => .style, + .title => .title, + .body => .body, + .html => .html, + .head => .head, + .unknown => .unknown, + }; }, - .svg => |se| se.getTag(), + .svg => self.subtype(Svg).getTag(), }; } @@ -2353,22 +2383,21 @@ pub const Build = struct { // Calls `func_name` with `args` on the most specific type where it is // implement. This could be on the Element itself. pub fn call(self: *const Element, comptime func_name: []const u8, args: anytype) !bool { - inline for (@typeInfo(Element.Type).@"union".fields) |f| { - if (@field(Element.Type, f.name) == self._type) { - // The inner type implements this function. Call it and we're done. - const S = reflect.Struct(f.type); + switch (self._type) { + inline else => |tag| { + const S = Subtype(tag); if (@hasDecl(S, "Build")) { + // The inner type has its own "call" method. Defer to it. if (@hasDecl(S.Build, "call")) { - const sub = @field(self._type, f.name); - return S.Build.call(sub, func_name, args); + return S.Build.call(self.subtype(S), func_name, args); } // The inner type implements this function. Call it and we're done. - if (@hasDecl(f.type, func_name)) { - return @call(.auto, @field(f.type, func_name), args); + if (@hasDecl(S, func_name)) { + return @call(.auto, @field(S, func_name), args); } } - } + }, } if (@hasDecl(Element.Build, func_name)) { diff --git a/src/browser/webapi/collections/node_live.zig b/src/browser/webapi/collections/node_live.zig index c96f9b179..884aefaf6 100644 --- a/src/browser/webapi/collections/node_live.zig +++ b/src/browser/webapi/collections/node_live.zig @@ -407,7 +407,7 @@ pub fn NodeLive(comptime mode: Mode) type { fn isFormControl(el: *Element) bool { if (el._type != .html) return false; - const html = el._type.html; + const html = el.subtype(Element.Html); return switch (html._type) { .input, .button, .select, .textarea => true, else => false, diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 80121f58d..862c89ff2 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -802,8 +802,8 @@ fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, name: String) []con fn getDefaultDisplay(element: *const Element) []const u8 { switch (element._type) { - .html => |html| { - return switch (html._type) { + .html => { + return switch (element.subtype(Element.Html)._type) { .anchor, .br, .span, .label, .time, .font, .mod, .quote => "inline", .body, .div, .dl, .p, .heading, .form, .button, .canvas, .details, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .frameset, .legend, .map, .marquee, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block", .generic, .custom, .unknown, .data => blk: { @@ -835,8 +835,8 @@ fn isInlineTag(tag_name: []const u8) bool { fn getDefaultColor(element: *const Element) []const u8 { switch (element._type) { - .html => |html| { - return switch (html._type) { + .html => { + return switch (element.subtype(Element.Html)._type) { .anchor => "rgb(0, 0, 238)", // blue else => "rgb(0, 0, 0)", }; diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 64e96684d..d8d6d0574 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -130,87 +130,169 @@ pub fn upgradeConstruct(frame: *Frame) !*Element { return node.is(Element) orelse return error.TypeError; } -pub const Type = union(enum) { - anchor: *Anchor, - area: *Area, - base: *Base, - body: *Body, - br: *BR, - button: *Button, - canvas: *Canvas, - custom: *Custom, - data: *Data, - datalist: *DataList, - details: *Details, - dialog: *Dialog, - directory: *Directory, - div: *Div, - dl: *DList, - embed: *Embed, - fieldset: *FieldSet, - font: *Font, - form: *Form, - frameset: *FrameSet, - generic: *Generic, - heading: *Heading, - head: *Head, - html: *Html, - hr: *HR, - img: *Image, - iframe: *IFrame, - input: *Input, - label: *Label, - legend: *Legend, - li: *LI, - link: *Link, - map: *Map, - marquee: *Marquee, - media: *Media, - meta: *Meta, - meter: *Meter, - mod: *Mod, - object: *Object, - ol: *OL, - optgroup: *OptGroup, - option: *Option, - output: *Output, - p: *Paragraph, - picture: *Picture, - param: *Param, - pre: *Pre, - progress: *Progress, - quote: *Quote, - script: *Script, - select: *Select, - slot: *Slot, - source: *Source, - span: *Span, - style: *Style, - table: *Table, - table_caption: *TableCaption, - table_cell: *TableCell, - table_col: *TableCol, - table_row: *TableRow, - table_section: *TableSection, - template: *Template, - textarea: *TextArea, - time: *Time, - title: *Title, - track: *Track, - ul: *UL, - unknown: *Unknown, +pub const Type = enum(u8) { + anchor, + area, + base, + body, + br, + button, + canvas, + custom, + data, + datalist, + details, + dialog, + directory, + div, + dl, + embed, + fieldset, + font, + form, + frameset, + generic, + heading, + head, + html, + hr, + img, + iframe, + input, + label, + legend, + li, + link, + map, + marquee, + media, + meta, + meter, + mod, + object, + ol, + optgroup, + option, + output, + p, + picture, + param, + pre, + progress, + quote, + script, + select, + slot, + source, + span, + style, + table, + table_caption, + table_cell, + table_col, + table_row, + table_section, + template, + textarea, + time, + title, + track, + ul, + unknown, }; +pub fn Subtype(comptime tag: Type) type { + return switch (tag) { + .anchor => Anchor, + .area => Area, + .base => Base, + .body => Body, + .br => BR, + .button => Button, + .canvas => Canvas, + .custom => Custom, + .data => Data, + .datalist => DataList, + .details => Details, + .dialog => Dialog, + .directory => Directory, + .div => Div, + .dl => DList, + .embed => Embed, + .fieldset => FieldSet, + .font => Font, + .form => Form, + .frameset => FrameSet, + .generic => Generic, + .heading => Heading, + .head => Head, + .html => Html, + .hr => HR, + .img => Image, + .iframe => IFrame, + .input => Input, + .label => Label, + .legend => Legend, + .li => LI, + .link => Link, + .map => Map, + .marquee => Marquee, + .media => Media, + .meta => Meta, + .meter => Meter, + .mod => Mod, + .object => Object, + .ol => OL, + .optgroup => OptGroup, + .option => Option, + .output => Output, + .p => Paragraph, + .picture => Picture, + .param => Param, + .pre => Pre, + .progress => Progress, + .quote => Quote, + .script => Script, + .select => Select, + .slot => Slot, + .source => Source, + .span => Span, + .style => Style, + .table => Table, + .table_caption => TableCaption, + .table_cell => TableCell, + .table_col => TableCol, + .table_row => TableRow, + .table_section => TableSection, + .template => Template, + .textarea => TextArea, + .time => Time, + .title => Title, + .track => Track, + .ul => UL, + .unknown => Unknown, + }; +} + +pub fn subtype(self: *const HtmlElement, comptime T: type) *T { + const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, HtmlElement); + const sub: *T = @ptrFromInt(@intFromPtr(self) + offset); + if (comptime lp.IS_DEBUG) { + // This pointer dance only works because the factory allocates the chain + // in a contiguous block of memory. In debug, we assert this holds via + // the _proto_canary back pointer. + std.debug.assert(Factory.protoOf(sub) == self); + } + return sub; +} + pub fn is(self: *HtmlElement, comptime T: type) ?*T { - inline for (@typeInfo(Type).@"union".fields) |f| { - if (@field(Type, f.name) == self._type) { - if (f.type == T) { - return &@field(self._type, f.name); + switch (self._type) { + inline else => |tag| { + if (Subtype(tag) == T) { + return self.subtype(T); } - if (f.type == *T) { - return @field(self._type, f.name); - } - } + }, } return null; } @@ -304,8 +386,8 @@ pub fn insertAdjacentHTML( pub fn click(self: *HtmlElement, frame: *Frame) !void { switch (self._type) { - inline .button, .input, .textarea, .select => |i| { - if (i.getDisabled()) { + inline .button, .input, .textarea, .select => |tag| { + if (self.subtype(Subtype(tag)).getDisabled()) { return; } }, @@ -330,8 +412,8 @@ pub fn click(self: *HtmlElement, frame: *Frame) !void { if (event._prevent_default == false) { // toggle the popover_target const explicit: ?*Element = switch (self._type) { - .button => |b| b._popover_target, - .input => |i| i._popover_target, + .button => self.subtype(Button)._popover_target, + .input => self.subtype(Input)._popover_target, else => null, }; try popover.runInvokerActivation(self, explicit, frame); @@ -543,7 +625,7 @@ fn setAttributeListener( ) !void { if (comptime lp.IS_DEBUG) { log.debug(.event, "Html.setAttributeListener", .{ - .type = std.meta.activeTag(self._type), + .type = self._type, .listener_type = listener_type, }); } @@ -1492,14 +1574,14 @@ fn collectInnerText(self: *HtmlElement, state: *InnerTextState) std.Io.Writer.Er const e = child.subtype(Node.Element); switch (e._type) { .svg => {}, - .html => |he| { + .html => { const tag = e.getTag(); switch (child_filter) { .none => {}, .select => if (tag != .option and tag != .optgroup) continue, .optgroup => if (tag != .option) continue, } - try handleChildElement(he, tag, state, &saw_cell, &saw_row); + try handleChildElement(e.subtype(HtmlElement), tag, state, &saw_cell, &saw_row); }, } }, @@ -1870,17 +1952,17 @@ pub const Build = struct { // Calls `func_name` with `args` on the most specific type where it is // implement. This could be on the HtmlElement itself. pub fn call(self: *const HtmlElement, comptime func_name: []const u8, args: anytype) !bool { - inline for (@typeInfo(HtmlElement.Type).@"union".fields) |f| { - if (@field(HtmlElement.Type, f.name) == self._type) { + switch (self._type) { + inline else => |tag| { + const S = Subtype(tag); // The inner type implements this function. Call it and we're done. - const S = reflect.Struct(f.type); if (@hasDecl(S, "Build")) { if (@hasDecl(S.Build, func_name)) { try @call(.auto, @field(S.Build, func_name), args); return true; } } - } + }, } if (@hasDecl(HtmlElement.Build, func_name)) { diff --git a/src/browser/webapi/element/Svg.zig b/src/browser/webapi/element/Svg.zig index a7a39cfe3..39960ec0b 100644 --- a/src/browser/webapi/element/Svg.zig +++ b/src/browser/webapi/element/Svg.zig @@ -16,6 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); @@ -47,64 +48,99 @@ _type: Type, _tag_name: String, // Svg elements are case-preserving _proto_canary: if (lp.IS_DEBUG) *Element else void = undefined, -pub const Type = union(enum) { - graphics: *Graphics, - view: *View, - title: *Title, - desc: *Desc, - metadata: *Metadata, - gradient: *GradientElement, - clip_path: *ClipPath, - marker: *Marker, - mask: *Mask, - pattern: *Pattern, - stop: *Stop, - generic: *Generic, +pub const Type = enum(u8) { + graphics, + view, + title, + desc, + metadata, + gradient, + clip_path, + marker, + mask, + pattern, + stop, + generic, }; +pub fn Subtype(comptime tag: Type) type { + return switch (tag) { + .graphics => Graphics, + .view => View, + .title => Title, + .desc => Desc, + .metadata => Metadata, + .gradient => GradientElement, + .clip_path => ClipPath, + .marker => Marker, + .mask => Mask, + .pattern => Pattern, + .stop => Stop, + .generic => Generic, + }; +} + +pub fn subtype(self: *const Svg, comptime T: type) *T { + const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, Svg); + const sub: *T = @ptrFromInt(@intFromPtr(self) + offset); + if (comptime lp.IS_DEBUG) { + // This pointer dance only works because the factory allocates the chain + // in a contiguous block of memory. In debug, we assert this holds via + // the _proto_canary back pointer. + std.debug.assert(Factory.protoOf(sub) == self); + } + return sub; +} + pub fn is(self: *Svg, comptime T: type) ?*T { - inline for (@typeInfo(Type).@"union".fields) |field| { - if (@field(Type, field.name) == self._type) { - if (field.type == *T) { - return @field(self._type, field.name); + switch (self._type) { + inline else => |tag| { + if (Subtype(tag) == T) { + return self.subtype(T); } - } + }, } if (self._type == .graphics) { - return self._type.graphics.is(T); + return self.subtype(Graphics).is(T); } if (self._type == .gradient) { - return self._type.gradient.is(T); + return self.subtype(GradientElement).is(T); } return null; } pub fn getTag(self: *const Svg) Element.Tag { return switch (self._type) { - .graphics => |g| switch (g._type) { - .svg => .svg, - .g => .g, - // No dedicated Element.Tag values; tag-name matching falls back - // to _tag_name, like it does for generic SVG elements. - .a, .use, .image, .defs, .symbol, .switch_element, .foreign_object => .unknown, - .text_content => |content| switch (content._type) { - .positioning => |positioning| switch (positioning._type) { - .text => .text, - .tspan => .unknown, + .graphics => blk: { + const g = self.subtype(Graphics); + break :blk switch (g._type) { + .svg => .svg, + .g => .g, + // No dedicated Element.Tag values; tag-name matching falls back + // to _tag_name, like it does for generic SVG elements. + .a, .use, .image, .defs, .symbol, .switch_element, .foreign_object => .unknown, + .text_content => tc: { + const content = g.subtype(Graphics.TextContent); + break :tc switch (content._type) { + .positioning => switch (content.subtype(Graphics.TextContent.TextPositioning)._type) { + .text => .text, + .tspan => .unknown, + }, + .text_path => .unknown, + }; }, - .text_path => .unknown, - }, - .geometry => |geo| switch (geo._type) { - .rect => .rect, - .circle => .circle, - .ellipse => .ellipse, - .line => .line, - .path => .path, - .polygon => .polygon, - .polyline => .polyline, - }, + .geometry => switch (g.subtype(Graphics.Geometry)._type) { + .rect => .rect, + .circle => .circle, + .ellipse => .ellipse, + .line => .line, + .path => .path, + .polygon => .polygon, + .polyline => .polyline, + }, + }; }, - .generic => |g| g._tag, + .generic => self.subtype(Generic)._tag, .title => .title, .view, .desc, .metadata, .gradient, .clip_path, .marker, .mask, .pattern, .stop => .unknown, }; diff --git a/src/browser/webapi/element/html/Label.zig b/src/browser/webapi/element/html/Label.zig index 7616c0e2c..fd4da498f 100644 --- a/src/browser/webapi/element/html/Label.zig +++ b/src/browser/webapi/element/html/Label.zig @@ -49,7 +49,7 @@ fn isLabelable(el: *Element) bool { const html = el.is(HtmlElement) orelse return false; return switch (html._type) { .button, .meter, .output, .progress, .select, .textarea => true, - .input => |input| input._input_type != .hidden, + .input => html.subtype(HtmlElement.Input)._input_type != .hidden, else => false, }; } diff --git a/src/browser/webapi/element/html/Media.zig b/src/browser/webapi/element/html/Media.zig index 8e3af790c..823dcee38 100644 --- a/src/browser/webapi/element/html/Media.zig +++ b/src/browser/webapi/element/html/Media.zig @@ -49,12 +49,34 @@ pub const NetworkState = enum(u16) { NETWORK_NO_SOURCE = 3, }; -pub const Type = union(enum) { +pub const Type = enum(u8) { generic, - audio: *Audio, - video: *Video, + audio, + video, }; +// `.generic` maps to Media itself: a bare chain ends at Media, so the +// tag has no chain member of its own. +pub fn Subtype(comptime tag: Type) type { + return switch (tag) { + .generic => Media, + .audio => Audio, + .video => Video, + }; +} + +pub fn subtype(self: *const Media, comptime T: type) *T { + const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, Media); + const sub: *T = @ptrFromInt(@intFromPtr(self) + offset); + if (comptime lp.IS_DEBUG) { + // This pointer dance only works because the factory allocates the chain + // in a contiguous block of memory. In debug, we assert this holds via + // the _proto_canary back pointer. + std.debug.assert(Factory.protoOf(sub) == self); + } + return sub; +} + _type: Type, _proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined, _paused: bool = true, @@ -77,18 +99,10 @@ pub fn asNode(self: *Media) *Node { } pub fn is(self: *Media, comptime T: type) ?*T { - const type_name = @typeName(T); switch (self._type) { - .audio => |a| { - if (T == *Audio) return a; - if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.Audio")) { - return a; - } - }, - .video => |v| { - if (T == *Video) return v; - if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.Video")) { - return v; + inline .audio, .video => |tag| { + if (Subtype(tag) == T) { + return self.subtype(T); } }, .generic => {}, diff --git a/src/browser/webapi/element/popover.zig b/src/browser/webapi/element/popover.zig index 68c490bc4..4912473de 100644 --- a/src/browser/webapi/element/popover.zig +++ b/src/browser/webapi/element/popover.zig @@ -207,7 +207,7 @@ pub fn invokerTarget(invoker: *Node, explicit: ?*Element, frame: *Frame) ?*Eleme pub fn runInvokerActivation(invoker: *HtmlElement, explicit: ?*Element, frame: *Frame) !void { switch (invoker._type) { .button => {}, - .input => |input| switch (input._input_type) { + .input => switch (invoker.subtype(HtmlElement.Input)._input_type) { .button, .submit, .reset, .image => {}, else => return, // not an invoker }, diff --git a/src/browser/webapi/element/svg/Geometry.zig b/src/browser/webapi/element/svg/Geometry.zig index 7df09b2ce..5f54155da 100644 --- a/src/browser/webapi/element/svg/Geometry.zig +++ b/src/browser/webapi/element/svg/Geometry.zig @@ -45,23 +45,47 @@ pub const Proto = Graphics; _type: Type, _proto_canary: if (lp.IS_DEBUG) *Graphics else void = undefined, -pub const Type = union(enum) { - rect: *Rect, - circle: *Circle, - ellipse: *Ellipse, - line: *Line, - path: *Path, - polygon: *Polygon, - polyline: *Polyline, +pub const Type = enum(u8) { + rect, + circle, + ellipse, + line, + path, + polygon, + polyline, }; +pub fn Subtype(comptime tag: Type) type { + return switch (tag) { + .rect => Rect, + .circle => Circle, + .ellipse => Ellipse, + .line => Line, + .path => Path, + .polygon => Polygon, + .polyline => Polyline, + }; +} + +pub fn subtype(self: *const Geometry, comptime T: type) *T { + const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, Geometry); + const sub: *T = @ptrFromInt(@intFromPtr(self) + offset); + if (comptime lp.IS_DEBUG) { + // This pointer dance only works because the factory allocates the chain + // in a contiguous block of memory. In debug, we assert this holds via + // the _proto_canary back pointer. + std.debug.assert(Factory.protoOf(sub) == self); + } + return sub; +} + pub fn is(self: *Geometry, comptime T: type) ?*T { - inline for (@typeInfo(Type).@"union".fields) |f| { - if (@field(Type, f.name) == self._type) { - if (f.type == *T) { - return @field(self._type, f.name); + switch (self._type) { + inline else => |tag| { + if (Subtype(tag) == T) { + return self.subtype(T); } - } + }, } return null; } @@ -107,16 +131,16 @@ pub fn getPointAtLength(self: *Geometry, distance: f64, frame: *Frame) !*DOMPoin pub fn buildPath(self: *Geometry, frame: *Frame) !PathData.Path { return switch (self._type) { - .rect => |rect| buildRect(rect, frame), - .circle => |circle| buildCircle(circle, frame), - .ellipse => |ellipse| buildEllipse(ellipse, frame), - .line => |line| buildLine(line, frame), - .path => |path| PathData.parse( - path.asElement().getAttributeSafe(comptime .wrap("d")) orelse "", + .rect => buildRect(self.subtype(Rect), frame), + .circle => buildCircle(self.subtype(Circle), frame), + .ellipse => buildEllipse(self.subtype(Ellipse), frame), + .line => buildLine(self.subtype(Line), frame), + .path => PathData.parse( + self.subtype(Path).asElement().getAttributeSafe(comptime .wrap("d")) orelse "", frame.local_arena, ), - .polygon => |polygon| buildPoints(try polygon.getPoints(frame), true, frame), - .polyline => |polyline| buildPoints(try polyline.getPoints(frame), false, frame), + .polygon => buildPoints(try self.subtype(Polygon).getPoints(frame), true, frame), + .polyline => buildPoints(try self.subtype(Polyline).getPoints(frame), false, frame), }; } diff --git a/src/browser/webapi/element/svg/GradientElement.zig b/src/browser/webapi/element/svg/GradientElement.zig index c63d460df..26eabb90f 100644 --- a/src/browser/webapi/element/svg/GradientElement.zig +++ b/src/browser/webapi/element/svg/GradientElement.zig @@ -16,6 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../../js/js.zig"); const Frame = @import("../../../Frame.zig"); @@ -39,18 +40,37 @@ pub const Proto = Svg; _type: Type, _proto_canary: if (lp.IS_DEBUG) *Svg else void = undefined, -pub const Type = union(enum) { - linear: *LinearGradient, - radial: *RadialGradient, +pub const Type = enum(u8) { + linear, + radial, }; +pub fn Subtype(comptime tag: Type) type { + return switch (tag) { + .linear => LinearGradient, + .radial => RadialGradient, + }; +} + +pub fn subtype(self: *const GradientElement, comptime T: type) *T { + const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, GradientElement); + const sub: *T = @ptrFromInt(@intFromPtr(self) + offset); + if (comptime lp.IS_DEBUG) { + // This pointer dance only works because the factory allocates the chain + // in a contiguous block of memory. In debug, we assert this holds via + // the _proto_canary back pointer. + std.debug.assert(Factory.protoOf(sub) == self); + } + return sub; +} + pub fn is(self: *GradientElement, comptime T: type) ?*T { - inline for (@typeInfo(Type).@"union".fields) |field| { - if (@field(Type, field.name) == self._type) { - if (field.type == *T) { - return @field(self._type, field.name); + switch (self._type) { + inline else => |tag| { + if (Subtype(tag) == T) { + return self.subtype(T); } - } + }, } return null; } diff --git a/src/browser/webapi/element/svg/Graphics.zig b/src/browser/webapi/element/svg/Graphics.zig index 8668ba3b1..e4b4f9d05 100644 --- a/src/browser/webapi/element/svg/Graphics.zig +++ b/src/browser/webapi/element/svg/Graphics.zig @@ -49,33 +49,61 @@ pub const Proto = SvgElement; _type: Type, _proto_canary: if (lp.IS_DEBUG) *SvgElement else void = undefined, -pub const Type = union(enum) { - svg: *Svg, - g: *G, - a: *A, - use: *Use, - image: *Image, - defs: *Defs, - symbol: *Symbol, - switch_element: *Switch, - foreign_object: *ForeignObject, - text_content: *TextContent, - geometry: *Geometry, +pub const Type = enum(u8) { + svg, + g, + a, + use, + image, + defs, + symbol, + switch_element, + foreign_object, + text_content, + geometry, }; +pub fn Subtype(comptime tag: Type) type { + return switch (tag) { + .svg => Svg, + .g => G, + .a => A, + .use => Use, + .image => Image, + .defs => Defs, + .symbol => Symbol, + .switch_element => Switch, + .foreign_object => ForeignObject, + .text_content => TextContent, + .geometry => Geometry, + }; +} + +pub fn subtype(self: *const Graphics, comptime T: type) *T { + const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, Graphics); + const sub: *T = @ptrFromInt(@intFromPtr(self) + offset); + if (comptime lp.IS_DEBUG) { + // This pointer dance only works because the factory allocates the chain + // in a contiguous block of memory. In debug, we assert this holds via + // the _proto_canary back pointer. + std.debug.assert(Factory.protoOf(sub) == self); + } + return sub; +} + pub fn is(self: *Graphics, comptime T: type) ?*T { - inline for (@typeInfo(Type).@"union".fields) |f| { - if (@field(Type, f.name) == self._type) { - if (f.type == *T) { - return @field(self._type, f.name); + switch (self._type) { + inline else => |tag| { + if (Subtype(tag) == T) { + return self.subtype(T); } - } + }, } if (self._type == .geometry) { - return self._type.geometry.is(T); + return self.subtype(Geometry).is(T); } if (self._type == .text_content) { - return self._type.text_content.is(T); + return self.subtype(TextContent).is(T); } return null; } @@ -108,12 +136,12 @@ pub const JsApi = struct { pub fn getBBox(self: *Graphics, frame: *Frame) !*DOMRect { var bounds: PathData.Bounds = .{}; switch (self._type) { - .geometry => |geometry| { - var path = try geometry.buildPath(frame); + .geometry => { + var path = try self.subtype(Geometry).buildPath(frame); defer path.deinit(frame.local_arena); bounds = path.bounds(.{}); }, - .foreign_object => |foreign_object| bounds = try foreign_object.getBounds(frame), + .foreign_object => bounds = try self.subtype(ForeignObject).getBounds(frame), .g, .a, .svg => try accumulateChildren(self, .{}, &bounds, frame), .defs, .symbol, .switch_element, .use, .image, .text_content => {}, } @@ -156,13 +184,13 @@ fn accumulateChildren(parent: *Graphics, matrix: PathData.Matrix, bounds: *PathD const child_matrix = parent_matrix.multiply(transformMatrix(element)); switch (graphics._type) { - .geometry => |geometry| { - var path = try geometry.buildPath(frame); + .geometry => { + var path = try graphics.subtype(Geometry).buildPath(frame); defer path.deinit(frame.local_arena); bounds.merge(path.bounds(child_matrix)); }, - .foreign_object => |foreign_object| { - const child_bounds = try foreign_object.getBounds(frame); + .foreign_object => { + const child_bounds = try graphics.subtype(ForeignObject).getBounds(frame); if (!child_bounds.isEmpty()) { var foreign_path: PathData.Path = .{}; defer foreign_path.deinit(frame.local_arena); diff --git a/src/browser/webapi/element/svg/TextContent.zig b/src/browser/webapi/element/svg/TextContent.zig index 665453aca..4f8683fda 100644 --- a/src/browser/webapi/element/svg/TextContent.zig +++ b/src/browser/webapi/element/svg/TextContent.zig @@ -16,6 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../../js/js.zig"); const Frame = @import("../../../Frame.zig"); @@ -39,18 +40,39 @@ pub const Proto = Graphics; _type: Type, _proto_canary: if (lp.IS_DEBUG) *Graphics else void = undefined, -pub const Type = union(enum) { - positioning: *TextPositioning, - text_path: *TextPath, +pub const Type = enum(u8) { + positioning, + text_path, }; -pub fn is(self: *TextContent, comptime T: type) ?*T { - inline for (@typeInfo(Type).@"union".fields) |field| { - if (@field(Type, field.name) == self._type) { - if (field.type == *T) return @field(self._type, field.name); - } +pub fn Subtype(comptime tag: Type) type { + return switch (tag) { + .positioning => TextPositioning, + .text_path => TextPath, + }; +} + +pub fn subtype(self: *const TextContent, comptime T: type) *T { + const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, TextContent); + const sub: *T = @ptrFromInt(@intFromPtr(self) + offset); + if (comptime lp.IS_DEBUG) { + // This pointer dance only works because the factory allocates the chain + // in a contiguous block of memory. In debug, we assert this holds via + // the _proto_canary back pointer. + std.debug.assert(Factory.protoOf(sub) == self); } - if (self._type == .positioning) return self._type.positioning.is(T); + return sub; +} + +pub fn is(self: *TextContent, comptime T: type) ?*T { + switch (self._type) { + inline else => |tag| { + if (Subtype(tag) == T) { + return self.subtype(T); + } + }, + } + if (self._type == .positioning) return self.subtype(TextPositioning).is(T); return null; } diff --git a/src/browser/webapi/element/svg/TextPositioning.zig b/src/browser/webapi/element/svg/TextPositioning.zig index 91052aed8..61567a4ea 100644 --- a/src/browser/webapi/element/svg/TextPositioning.zig +++ b/src/browser/webapi/element/svg/TextPositioning.zig @@ -16,6 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../../js/js.zig"); @@ -34,16 +35,37 @@ pub const Proto = TextContent; _type: Type, _proto_canary: if (lp.IS_DEBUG) *TextContent else void = undefined, -pub const Type = union(enum) { - text: *Text, - tspan: *TSpan, +pub const Type = enum(u8) { + text, + tspan, }; +pub fn Subtype(comptime tag: Type) type { + return switch (tag) { + .text => Text, + .tspan => TSpan, + }; +} + +pub fn subtype(self: *const TextPositioning, comptime T: type) *T { + const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, TextPositioning); + const sub: *T = @ptrFromInt(@intFromPtr(self) + offset); + if (comptime lp.IS_DEBUG) { + // This pointer dance only works because the factory allocates the chain + // in a contiguous block of memory. In debug, we assert this holds via + // the _proto_canary back pointer. + std.debug.assert(Factory.protoOf(sub) == self); + } + return sub; +} + pub fn is(self: *TextPositioning, comptime T: type) ?*T { - inline for (@typeInfo(Type).@"union".fields) |field| { - if (@field(Type, field.name) == self._type) { - if (field.type == *T) return @field(self._type, field.name); - } + switch (self._type) { + inline else => |tag| { + if (Subtype(tag) == T) { + return self.subtype(T); + } + }, } return null; }