From 4165f47a648060c93be4d2b061a22304c6885ba7 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 4 Jun 2025 19:52:23 +0800 Subject: [PATCH] merge all states --- src/browser/State.zig | 65 +++++++++++++++++++++++++++++++ src/browser/browser.zig | 4 ++ src/browser/dom/document.zig | 5 +-- src/browser/html/document.zig | 7 ++-- src/browser/html/elements.zig | 11 +++--- src/browser/html/select.zig | 22 ++--------- src/browser/page.zig | 25 ++++++------ src/browser/session.zig | 1 + src/browser/state/Document.zig | 31 --------------- src/browser/state/HTMLElement.zig | 24 ------------ 10 files changed, 98 insertions(+), 97 deletions(-) create mode 100644 src/browser/State.zig delete mode 100644 src/browser/state/Document.zig delete mode 100644 src/browser/state/HTMLElement.zig diff --git a/src/browser/State.zig b/src/browser/State.zig new file mode 100644 index 00000000..55447923 --- /dev/null +++ b/src/browser/State.zig @@ -0,0 +1,65 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Sometimes we need to extend libdom. For example, its HTMLDocument doesn't +// have a readyState. We have a couple different options, such as making the +// correction in libdom directly. Another option stems from the fact that every +// libdom node has an opaque embedder_data field. This is the struct that we +// lazily load into that field. +// +// It didn't originally start off as a collection of every single extension, but +// this quickly proved necessary, since different fields are needed on the same +// data at different levels of the prototype chain. This isn't memory efficient. + +const Env = @import("env.zig").Env; +const parser = @import("netsurf.zig"); +const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration; + +// for HTMLScript (but probably needs to be added to more) +onload: ?Env.Function = null, +onerror: ?Env.Function = null, + +// for HTMLElement +style: CSSStyleDeclaration = .empty, + +// for html/document +ready_state: ReadyState = .loading, + +// for dom/document +active_element: ?*parser.Element = null, + +// for HTMLSelectElement +// By default, if no option is explicitly selected, the first option should +// be selected. However, libdom doesn't do this, and it sets the +// selectedIndex to -1, which is a valid value for "nothing selected". +// Therefore, when libdom says the selectedIndex == -1, we don't know if +// it means that nothing is selected, or if the first option is selected by +// default. +// There are cases where this won't work, but when selectedIndex is +// explicitly set, we set this boolean flag. Then, when we're getting then +// selectedIndex, if this flag is == false, which is to say that if +// selectedIndex hasn't been explicitly set AND if we have at least 1 option +// AND if it isn't a multi select, we can make the 1st item selected by +// default (by returning selectedIndex == 0). +explicit_index_set: bool = false, + +const ReadyState = enum { + loading, + interactive, + complete, +}; diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 0a4db620..8bf24e36 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -21,6 +21,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; +const State = @import("State.zig"); const Env = @import("env.zig").Env; const App = @import("../app.zig").App; const Session = @import("session.zig").Session; @@ -41,6 +42,7 @@ pub const Browser = struct { session_arena: ArenaAllocator, transfer_arena: ArenaAllocator, notification: *Notification, + state_pool: std.heap.MemoryPool(State), pub fn init(app: *App) !Browser { const allocator = app.allocator; @@ -61,6 +63,7 @@ pub const Browser = struct { .page_arena = ArenaAllocator.init(allocator), .session_arena = ArenaAllocator.init(allocator), .transfer_arena = ArenaAllocator.init(allocator), + .state_pool = std.heap.MemoryPool(State).init(allocator), }; } @@ -71,6 +74,7 @@ pub const Browser = struct { self.session_arena.deinit(); self.transfer_arena.deinit(); self.notification.deinit(); + self.state_pool.deinit(); } pub fn newSession(self: *Browser) !*Session { diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index f96e20bb..633f069f 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -28,7 +28,6 @@ const NodeUnion = @import("node.zig").Union; const collection = @import("html_collection.zig"); const css = @import("css.zig"); -const State = @import("../state/Document.zig"); const Element = @import("element.zig").Element; const ElementUnion = @import("element.zig").Union; const TreeWalker = @import("tree_walker.zig").TreeWalker; @@ -245,7 +244,7 @@ pub const Document = struct { } pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion { - const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self)); + const state = try page.getOrCreateNodeState(@ptrCast(self)); if (state.active_element) |ae| { return try Element.toInterface(ae); } @@ -262,7 +261,7 @@ pub const Document = struct { // we could look for the "disabled" attribute, but that's only meaningful // on certain types, and libdom's vtable doesn't seem to expose this. pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void { - const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self)); + const state = try page.getOrCreateNodeState(@ptrCast(self)); state.active_element = @ptrCast(e); } }; diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index 384bc9d7..240df53c 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -30,7 +30,6 @@ const NodeList = @import("../dom/nodelist.zig").NodeList; const Location = @import("location.zig").Location; const collection = @import("../dom/html_collection.zig"); -const State = @import("../state/Document.zig"); const Walker = @import("../dom/walker.zig").WalkerDepthFirst; const Cookie = @import("../storage/cookie.zig").Cookie; @@ -185,7 +184,7 @@ pub const HTMLDocument = struct { } pub fn get_readyState(self: *parser.DocumentHTML, page: *Page) ![]const u8 { - const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self)); + const state = try page.getOrCreateNodeState(@ptrCast(self)); return @tagName(state.ready_state); } @@ -264,7 +263,7 @@ pub const HTMLDocument = struct { } pub fn documentIsLoaded(self: *parser.DocumentHTML, page: *Page) !void { - const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self)); + const state = try page.getOrCreateNodeState(@ptrCast(self)); state.ready_state = .interactive; const evt = try parser.eventCreate(); @@ -279,7 +278,7 @@ pub const HTMLDocument = struct { } pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void { - const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self)); + const state = try page.getOrCreateNodeState(@ptrCast(self)); state.ready_state = .complete; } }; diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 5dae5c2a..15006bc5 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -25,7 +25,6 @@ const Page = @import("../page.zig").Page; const urlStitch = @import("../../url.zig").URL.stitch; const URL = @import("../url/url.zig").URL; const Node = @import("../dom/node.zig").Node; -const State = @import("../state/HTMLElement.zig"); const Element = @import("../dom/element.zig").Element; const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration; @@ -114,7 +113,7 @@ pub const HTMLElement = struct { pub const subtype = .node; pub fn get_style(e: *parser.ElementHTML, page: *Page) !*CSSStyleDeclaration { - const state = try page.getOrCreateNodeWrapper(State, @ptrCast(e)); + const state = try page.getOrCreateNodeState(@ptrCast(e)); return &state.style; } @@ -954,22 +953,22 @@ pub const HTMLScriptElement = struct { } pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function { - const state = page.getNodeWrapper(State, @ptrCast(self)) orelse return null; + const state = page.getNodeState(@ptrCast(self)) orelse return null; return state.onload; } pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void { - const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self)); + const state = try page.getOrCreateNodeState(@ptrCast(self)); state.onload = function; } pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function { - const state = page.getNodeWrapper(State, @ptrCast(self)) orelse return null; + const state = page.getNodeState(@ptrCast(self)) orelse return null; return state.onerror; } pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void { - const state = try page.getOrCreateNodeWrapper(State, @ptrCast(self)); + const state = try page.getOrCreateNodeState(@ptrCast(self)); state.onerror = function; } }; diff --git a/src/browser/html/select.zig b/src/browser/html/select.zig index 5a73138f..ce078630 100644 --- a/src/browser/html/select.zig +++ b/src/browser/html/select.zig @@ -26,20 +26,6 @@ pub const HTMLSelectElement = struct { pub const prototype = *HTMLElement; pub const subtype = .node; - // By default, if no option is explicitly selected, the first option should - // be selected. However, libdom doesn't do this, and it sets the - // selectedIndex to -1, which is a valid value for "nothing selected". - // Therefore, when libdom says the selectedIndex == -1, we don't know if - // it means that nothing is selected, or if the first option is selected by - // default. - // There are cases where this won't work, but when selectedIndex is - // explicitly set, we set this boolean flag. Then, when we're getting then - // selectedIndex, if this flag is == false, which is to say that if - // selectedIndex hasn't been explicitly set AND if we have at least 1 option - // AND if it isn't a multi select, we can make the 1st item selected by - // default (by returning selectedIndex == 0). - explicit_index_set: bool = false, - pub fn get_length(select: *parser.Select) !u32 { return parser.selectGetLength(select); } @@ -70,11 +56,11 @@ pub const HTMLSelectElement = struct { } pub fn get_selectedIndex(select: *parser.Select, page: *Page) !i32 { - const self = try page.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select)); + const state = try page.getOrCreateNodeState(@ptrCast(select)); const selected_index = try parser.selectGetSelectedIndex(select); // See the explicit_index_set field documentation - if (!self.explicit_index_set) { + if (!state.explicit_index_set) { if (selected_index == -1) { if (try parser.selectGetMultiple(select) == false) { if (try get_length(select) > 0) { @@ -89,8 +75,8 @@ pub const HTMLSelectElement = struct { // Libdom's dom_html_select_select_set_selected_index will crash if index // is out of range, and it doesn't properly unset options pub fn set_selectedIndex(select: *parser.Select, index: i32, page: *Page) !void { - var self = try page.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select)); - self.explicit_index_set = true; + var state = try page.getOrCreateNodeState(@ptrCast(select)); + state.explicit_index_set = true; const options = try parser.selectGetOptions(select); const len = try parser.optionCollectionGetLength(options); diff --git a/src/browser/page.zig b/src/browser/page.zig index fa8088a9..83f1136b 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -22,6 +22,7 @@ const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const Dump = @import("dump.zig"); +const State = @import("State.zig"); const Env = @import("env.zig").Env; const Mime = @import("mime.zig").Mime; const DataURI = @import("datauri.zig").DataURI; @@ -95,6 +96,8 @@ pub const Page = struct { // indicates intention to navigate to another page on the next loop execution. delayed_navigation: bool = false, + state_pool: *std.heap.MemoryPool(State), + pub fn init(self: *Page, arena: Allocator, session: *Session) !void { const browser = session.browser; self.* = .{ @@ -106,6 +109,7 @@ pub const Page = struct { .call_arena = undefined, .loop = browser.app.loop, .renderer = Renderer.init(arena), + .state_pool = &browser.state_pool, .cookie_jar = &session.cookie_jar, .microtask_node = .{ .func = microtaskCallback }, .window_clicked_event_node = .{ .func = windowClicked }, @@ -597,21 +601,21 @@ pub const Page = struct { _ = try self.loop.timeout(0, &navi.navigate_node); } - pub fn getOrCreateNodeWrapper(self: *Page, comptime T: type, node: *parser.Node) !*T { - if (self.getNodeWrapper(T, node)) |wrap| { + pub fn getOrCreateNodeState(self: *Page, node: *parser.Node) !*State { + if (self.getNodeState(node)) |wrap| { return wrap; } - const wrap = try self.arena.create(T); - wrap.* = T{}; + const state = try self.state_pool.create(); + state.* = .{}; - parser.nodeSetEmbedderData(node, wrap); - return wrap; + parser.nodeSetEmbedderData(node, state); + return state; } - pub fn getNodeWrapper(_: *const Page, comptime T: type, node: *parser.Node) ?*T { - if (parser.nodeGetEmbedderData(node)) |wrap| { - return @alignCast(@ptrCast(wrap)); + pub fn getNodeState(_: *const Page, node: *parser.Node) ?*State { + if (parser.nodeGetEmbedderData(node)) |state| { + return @alignCast(@ptrCast(state)); } return null; } @@ -743,8 +747,7 @@ const Script = struct { // attached to it. But this seems quite unlikely and it does help // optimize loading scripts, of which there can be hundreds for a // page. - const State = @import("state/HTMLElement.zig"); - if (page.getNodeWrapper(State, @ptrCast(e))) |se| { + if (page.getNodeState(@ptrCast(e))) |se| { if (se.onload) |function| { onload = .{ .function = function }; } diff --git a/src/browser/session.zig b/src/browser/session.zig index b128d14b..44fce9d3 100644 --- a/src/browser/session.zig +++ b/src/browser/session.zig @@ -90,6 +90,7 @@ pub const Session = struct { const page_arena = &self.browser.page_arena; _ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); + _ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 }); self.page = @as(Page, undefined); const page = &self.page.?; diff --git a/src/browser/state/Document.zig b/src/browser/state/Document.zig deleted file mode 100644 index dc2b6050..00000000 --- a/src/browser/state/Document.zig +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const parser = @import("../netsurf.zig"); - -// proxy-owner for html/document -ready_state: ReadyState = .loading, - -// proxy-owner for dom/document -active_element: ?*parser.Element = null, - -const ReadyState = enum { - loading, - interactive, - complete, -}; diff --git a/src/browser/state/HTMLElement.zig b/src/browser/state/HTMLElement.zig deleted file mode 100644 index 0a41b207..00000000 --- a/src/browser/state/HTMLElement.zig +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const Env = @import("../env.zig").Env; -const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration; - -onload: ?Env.Function = null, -onerror: ?Env.Function = null, -style: CSSStyleDeclaration = .empty,