Skip to content

Commit 8de57ec

Browse files
authored
Merge pull request #761 from lightpanda-io/pozo_for_custom_state
Improve usability of NodeWrapper
2 parents f931026 + 4165f47 commit 8de57ec

File tree

8 files changed

+127
-81
lines changed

8 files changed

+127
-81
lines changed

src/browser/State.zig

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
// Sometimes we need to extend libdom. For example, its HTMLDocument doesn't
20+
// have a readyState. We have a couple different options, such as making the
21+
// correction in libdom directly. Another option stems from the fact that every
22+
// libdom node has an opaque embedder_data field. This is the struct that we
23+
// lazily load into that field.
24+
//
25+
// It didn't originally start off as a collection of every single extension, but
26+
// this quickly proved necessary, since different fields are needed on the same
27+
// data at different levels of the prototype chain. This isn't memory efficient.
28+
29+
const Env = @import("env.zig").Env;
30+
const parser = @import("netsurf.zig");
31+
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
32+
33+
// for HTMLScript (but probably needs to be added to more)
34+
onload: ?Env.Function = null,
35+
onerror: ?Env.Function = null,
36+
37+
// for HTMLElement
38+
style: CSSStyleDeclaration = .empty,
39+
40+
// for html/document
41+
ready_state: ReadyState = .loading,
42+
43+
// for dom/document
44+
active_element: ?*parser.Element = null,
45+
46+
// for HTMLSelectElement
47+
// By default, if no option is explicitly selected, the first option should
48+
// be selected. However, libdom doesn't do this, and it sets the
49+
// selectedIndex to -1, which is a valid value for "nothing selected".
50+
// Therefore, when libdom says the selectedIndex == -1, we don't know if
51+
// it means that nothing is selected, or if the first option is selected by
52+
// default.
53+
// There are cases where this won't work, but when selectedIndex is
54+
// explicitly set, we set this boolean flag. Then, when we're getting then
55+
// selectedIndex, if this flag is == false, which is to say that if
56+
// selectedIndex hasn't been explicitly set AND if we have at least 1 option
57+
// AND if it isn't a multi select, we can make the 1st item selected by
58+
// default (by returning selectedIndex == 0).
59+
explicit_index_set: bool = false,
60+
61+
const ReadyState = enum {
62+
loading,
63+
interactive,
64+
complete,
65+
};

src/browser/browser.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const std = @import("std");
2121
const Allocator = std.mem.Allocator;
2222
const ArenaAllocator = std.heap.ArenaAllocator;
2323

24+
const State = @import("State.zig");
2425
const Env = @import("env.zig").Env;
2526
const App = @import("../app.zig").App;
2627
const Session = @import("session.zig").Session;
@@ -41,6 +42,7 @@ pub const Browser = struct {
4142
session_arena: ArenaAllocator,
4243
transfer_arena: ArenaAllocator,
4344
notification: *Notification,
45+
state_pool: std.heap.MemoryPool(State),
4446

4547
pub fn init(app: *App) !Browser {
4648
const allocator = app.allocator;
@@ -61,6 +63,7 @@ pub const Browser = struct {
6163
.page_arena = ArenaAllocator.init(allocator),
6264
.session_arena = ArenaAllocator.init(allocator),
6365
.transfer_arena = ArenaAllocator.init(allocator),
66+
.state_pool = std.heap.MemoryPool(State).init(allocator),
6467
};
6568
}
6669

@@ -71,6 +74,7 @@ pub const Browser = struct {
7174
self.session_arena.deinit();
7275
self.transfer_arena.deinit();
7376
self.notification.deinit();
77+
self.state_pool.deinit();
7478
}
7579

7680
pub fn newSession(self: *Browser) !*Session {

src/browser/dom/document.zig

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ pub const Document = struct {
4242
pub const prototype = *Node;
4343
pub const subtype = .node;
4444

45-
active_element: ?*parser.Element = null,
46-
4745
pub fn constructor(page: *const Page) !*parser.DocumentHTML {
4846
const doc = try parser.documentCreateDocument(
4947
try parser.documentHTMLGetTitle(page.window.document),
@@ -245,17 +243,26 @@ pub const Document = struct {
245243
return try TreeWalker.init(root, what_to_show, filter);
246244
}
247245

248-
pub fn get_activeElement(doc: *parser.Document, page: *Page) !?ElementUnion {
249-
const self = try page.getOrCreateNodeWrapper(Document, @ptrCast(doc));
250-
if (self.active_element) |ae| {
246+
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
247+
const state = try page.getOrCreateNodeState(@ptrCast(self));
248+
if (state.active_element) |ae| {
251249
return try Element.toInterface(ae);
252250
}
253251

254252
if (try parser.documentHTMLBody(page.window.document)) |body| {
255253
return try Element.toInterface(@ptrCast(body));
256254
}
257255

258-
return get_documentElement(doc);
256+
return get_documentElement(self);
257+
}
258+
259+
// TODO: some elements can't be focused, like if they're disabled
260+
// but there doesn't seem to be a generic way to check this. For example
261+
// we could look for the "disabled" attribute, but that's only meaningful
262+
// on certain types, and libdom's vtable doesn't seem to expose this.
263+
pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void {
264+
const state = try page.getOrCreateNodeState(@ptrCast(self));
265+
state.active_element = @ptrCast(e);
259266
}
260267
};
261268

src/browser/html/document.zig

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,6 @@ pub const HTMLDocument = struct {
3939
pub const prototype = *Document;
4040
pub const subtype = .node;
4141

42-
ready_state: ReadyState = .loading,
43-
44-
const ReadyState = enum {
45-
loading,
46-
interactive,
47-
complete,
48-
};
49-
5042
// JS funcs
5143
// --------
5244

@@ -191,9 +183,9 @@ pub const HTMLDocument = struct {
191183
return &page.window;
192184
}
193185

194-
pub fn get_readyState(node: *parser.DocumentHTML, page: *Page) ![]const u8 {
195-
const self = try page.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(node));
196-
return @tagName(self.ready_state);
186+
pub fn get_readyState(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
187+
const state = try page.getOrCreateNodeState(@ptrCast(self));
188+
return @tagName(state.ready_state);
197189
}
198190

199191
// noop legacy functions
@@ -270,9 +262,9 @@ pub const HTMLDocument = struct {
270262
return list.items;
271263
}
272264

273-
pub fn documentIsLoaded(html_doc: *parser.DocumentHTML, page: *Page) !void {
274-
const self = try page.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc));
275-
self.ready_state = .interactive;
265+
pub fn documentIsLoaded(self: *parser.DocumentHTML, page: *Page) !void {
266+
const state = try page.getOrCreateNodeState(@ptrCast(self));
267+
state.ready_state = .interactive;
276268

277269
const evt = try parser.eventCreate();
278270
defer parser.eventDestroy(evt);
@@ -282,12 +274,12 @@ pub const HTMLDocument = struct {
282274
.source = "document",
283275
});
284276
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
285-
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
277+
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, self), evt);
286278
}
287279

288-
pub fn documentIsComplete(html_doc: *parser.DocumentHTML, page: *Page) !void {
289-
const self = try page.getOrCreateNodeWrapper(HTMLDocument, @ptrCast(html_doc));
290-
self.ready_state = .complete;
280+
pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
281+
const state = try page.getOrCreateNodeState(@ptrCast(self));
282+
state.ready_state = .complete;
291283
}
292284
};
293285

src/browser/html/elements.zig

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,9 @@ pub const HTMLElement = struct {
112112
pub const prototype = *Element;
113113
pub const subtype = .node;
114114

115-
style: CSSStyleDeclaration = .empty,
116-
117115
pub fn get_style(e: *parser.ElementHTML, page: *Page) !*CSSStyleDeclaration {
118-
const self = try page.getOrCreateNodeWrapper(HTMLElement, @ptrCast(e));
119-
return &self.style;
116+
const state = try page.getOrCreateNodeState(@ptrCast(e));
117+
return &state.style;
120118
}
121119

122120
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
@@ -159,16 +157,9 @@ pub const HTMLElement = struct {
159157
return;
160158
}
161159

162-
const root_node = try parser.nodeGetRootNode(@ptrCast(e));
163-
164160
const Document = @import("../dom/document.zig").Document;
165-
const document = try page.getOrCreateNodeWrapper(Document, @ptrCast(root_node));
166-
167-
// TODO: some elements can't be focused, like if they're disabled
168-
// but there doesn't seem to be a generic way to check this. For example
169-
// we could look for the "disabled" attribute, but that's only meaningful
170-
// on certain types, and libdom's vtable doesn't seem to expose this.
171-
document.active_element = @ptrCast(e);
161+
const root_node = try parser.nodeGetRootNode(@ptrCast(e));
162+
try Document.setFocus(@ptrCast(root_node), e, page);
172163
}
173164
};
174165

@@ -852,9 +843,6 @@ pub const HTMLScriptElement = struct {
852843
pub const prototype = *HTMLElement;
853844
pub const subtype = .node;
854845

855-
onload: ?Env.Function = null,
856-
onerror: ?Env.Function = null,
857-
858846
pub fn get_src(self: *parser.Script) !?[]const u8 {
859847
return try parser.elementGetAttribute(
860848
parser.scriptToElt(self),
@@ -964,24 +952,24 @@ pub const HTMLScriptElement = struct {
964952
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
965953
}
966954

967-
pub fn get_onload(script: *parser.Script, page: *Page) !?Env.Function {
968-
const self = page.getNodeWrapper(HTMLScriptElement, @ptrCast(script)) orelse return null;
969-
return self.onload;
955+
pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
956+
const state = page.getNodeState(@ptrCast(self)) orelse return null;
957+
return state.onload;
970958
}
971959

972-
pub fn set_onload(script: *parser.Script, function: ?Env.Function, page: *Page) !void {
973-
const self = try page.getOrCreateNodeWrapper(HTMLScriptElement, @ptrCast(script));
974-
self.onload = function;
960+
pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
961+
const state = try page.getOrCreateNodeState(@ptrCast(self));
962+
state.onload = function;
975963
}
976964

977-
pub fn get_onerror(script: *parser.Script, page: *Page) !?Env.Function {
978-
const self = page.getNodeWrapper(HTMLScriptElement, @ptrCast(script)) orelse return null;
979-
return self.onerror;
965+
pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
966+
const state = page.getNodeState(@ptrCast(self)) orelse return null;
967+
return state.onerror;
980968
}
981969

982-
pub fn set_onerror(script: *parser.Script, function: ?Env.Function, page: *Page) !void {
983-
const self = try page.getOrCreateNodeWrapper(HTMLScriptElement, @ptrCast(script));
984-
self.onerror = function;
970+
pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
971+
const state = try page.getOrCreateNodeState(@ptrCast(self));
972+
state.onerror = function;
985973
}
986974
};
987975

src/browser/html/select.zig

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,6 @@ pub const HTMLSelectElement = struct {
2626
pub const prototype = *HTMLElement;
2727
pub const subtype = .node;
2828

29-
// By default, if no option is explicitly selected, the first option should
30-
// be selected. However, libdom doesn't do this, and it sets the
31-
// selectedIndex to -1, which is a valid value for "nothing selected".
32-
// Therefore, when libdom says the selectedIndex == -1, we don't know if
33-
// it means that nothing is selected, or if the first option is selected by
34-
// default.
35-
// There are cases where this won't work, but when selectedIndex is
36-
// explicitly set, we set this boolean flag. Then, when we're getting then
37-
// selectedIndex, if this flag is == false, which is to say that if
38-
// selectedIndex hasn't been explicitly set AND if we have at least 1 option
39-
// AND if it isn't a multi select, we can make the 1st item selected by
40-
// default (by returning selectedIndex == 0).
41-
explicit_index_set: bool = false,
42-
4329
pub fn get_length(select: *parser.Select) !u32 {
4430
return parser.selectGetLength(select);
4531
}
@@ -70,11 +56,11 @@ pub const HTMLSelectElement = struct {
7056
}
7157

7258
pub fn get_selectedIndex(select: *parser.Select, page: *Page) !i32 {
73-
const self = try page.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select));
59+
const state = try page.getOrCreateNodeState(@ptrCast(select));
7460
const selected_index = try parser.selectGetSelectedIndex(select);
7561

7662
// See the explicit_index_set field documentation
77-
if (!self.explicit_index_set) {
63+
if (!state.explicit_index_set) {
7864
if (selected_index == -1) {
7965
if (try parser.selectGetMultiple(select) == false) {
8066
if (try get_length(select) > 0) {
@@ -89,8 +75,8 @@ pub const HTMLSelectElement = struct {
8975
// Libdom's dom_html_select_select_set_selected_index will crash if index
9076
// is out of range, and it doesn't properly unset options
9177
pub fn set_selectedIndex(select: *parser.Select, index: i32, page: *Page) !void {
92-
var self = try page.getOrCreateNodeWrapper(HTMLSelectElement, @ptrCast(select));
93-
self.explicit_index_set = true;
78+
var state = try page.getOrCreateNodeState(@ptrCast(select));
79+
state.explicit_index_set = true;
9480

9581
const options = try parser.selectGetOptions(select);
9682
const len = try parser.optionCollectionGetLength(options);

src/browser/page.zig

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const builtin = @import("builtin");
2222
const Allocator = std.mem.Allocator;
2323

2424
const Dump = @import("dump.zig");
25+
const State = @import("State.zig");
2526
const Env = @import("env.zig").Env;
2627
const Mime = @import("mime.zig").Mime;
2728
const DataURI = @import("datauri.zig").DataURI;
@@ -95,6 +96,8 @@ pub const Page = struct {
9596
// indicates intention to navigate to another page on the next loop execution.
9697
delayed_navigation: bool = false,
9798

99+
state_pool: *std.heap.MemoryPool(State),
100+
98101
pub fn init(self: *Page, arena: Allocator, session: *Session) !void {
99102
const browser = session.browser;
100103
self.* = .{
@@ -106,6 +109,7 @@ pub const Page = struct {
106109
.call_arena = undefined,
107110
.loop = browser.app.loop,
108111
.renderer = Renderer.init(arena),
112+
.state_pool = &browser.state_pool,
109113
.cookie_jar = &session.cookie_jar,
110114
.microtask_node = .{ .func = microtaskCallback },
111115
.window_clicked_event_node = .{ .func = windowClicked },
@@ -597,21 +601,21 @@ pub const Page = struct {
597601
_ = try self.loop.timeout(0, &navi.navigate_node);
598602
}
599603

600-
pub fn getOrCreateNodeWrapper(self: *Page, comptime T: type, node: *parser.Node) !*T {
601-
if (self.getNodeWrapper(T, node)) |wrap| {
604+
pub fn getOrCreateNodeState(self: *Page, node: *parser.Node) !*State {
605+
if (self.getNodeState(node)) |wrap| {
602606
return wrap;
603607
}
604608

605-
const wrap = try self.arena.create(T);
606-
wrap.* = T{};
609+
const state = try self.state_pool.create();
610+
state.* = .{};
607611

608-
parser.nodeSetEmbedderData(node, wrap);
609-
return wrap;
612+
parser.nodeSetEmbedderData(node, state);
613+
return state;
610614
}
611615

612-
pub fn getNodeWrapper(_: *const Page, comptime T: type, node: *parser.Node) ?*T {
613-
if (parser.nodeGetEmbedderData(node)) |wrap| {
614-
return @alignCast(@ptrCast(wrap));
616+
pub fn getNodeState(_: *const Page, node: *parser.Node) ?*State {
617+
if (parser.nodeGetEmbedderData(node)) |state| {
618+
return @alignCast(@ptrCast(state));
615619
}
616620
return null;
617621
}
@@ -743,8 +747,7 @@ const Script = struct {
743747
// attached to it. But this seems quite unlikely and it does help
744748
// optimize loading scripts, of which there can be hundreds for a
745749
// page.
746-
const HTMLScriptElement = @import("html/elements.zig").HTMLScriptElement;
747-
if (page.getNodeWrapper(HTMLScriptElement, @ptrCast(e))) |se| {
750+
if (page.getNodeState(@ptrCast(e))) |se| {
748751
if (se.onload) |function| {
749752
onload = .{ .function = function };
750753
}

src/browser/session.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ pub const Session = struct {
9090

9191
const page_arena = &self.browser.page_arena;
9292
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
93+
_ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 });
9394

9495
self.page = @as(Page, undefined);
9596
const page = &self.page.?;

0 commit comments

Comments
 (0)