mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 14:33:47 +00:00
start handling page clicks and key presses
This commit is contained in:
@@ -191,6 +191,19 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
||||
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
|
||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||
|
||||
// Defer runs even on early return - ensures event phase is reset
|
||||
// and default actions execute (unless prevented)
|
||||
defer {
|
||||
event._event_phase = .none;
|
||||
|
||||
// Execute default action if not prevented
|
||||
if (!event._prevent_default and event._type_string.eqlSlice("click")) {
|
||||
self.page.handleClick(target) catch |err| {
|
||||
log.warn(.event, "page.click", .{ .err = err });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var path_len: usize = 0;
|
||||
var path_buffer: [128]*EventTarget = undefined;
|
||||
|
||||
@@ -236,7 +249,6 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
if (self.lookup.getPtr(@intFromPtr(current_target))) |list| {
|
||||
try self.dispatchPhase(list, current_target, event, was_handled, true);
|
||||
if (event._stop_propagation) {
|
||||
event._event_phase = .none;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -248,7 +260,6 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
if (self.lookup.getPtr(@intFromPtr(target_et))) |list| {
|
||||
try self.dispatchPhase(list, target_et, event, was_handled, null);
|
||||
if (event._stop_propagation) {
|
||||
event._event_phase = .none;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -266,8 +277,6 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event._event_phase = .none;
|
||||
}
|
||||
|
||||
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void {
|
||||
|
||||
@@ -59,6 +59,7 @@ const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
|
||||
const storage = @import("webapi/storage/storage.zig");
|
||||
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
|
||||
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
||||
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
||||
|
||||
const timestamp = @import("../datetime.zig").timestamp;
|
||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||
@@ -2270,19 +2271,6 @@ fn nodeIsReady(self: *Page, comptime from_parser: bool, node: *Node) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn asUint(comptime string: anytype) std.meta.Int(
|
||||
.unsigned,
|
||||
@bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
|
||||
) {
|
||||
const byteLength = @sizeOf(@TypeOf(string.*)) - 1;
|
||||
const expectedType = *const [byteLength:0]u8;
|
||||
if (@TypeOf(string) != expectedType) {
|
||||
@compileError("expected : " ++ @typeName(expectedType) ++ ", got: " ++ @typeName(@TypeOf(string)));
|
||||
}
|
||||
|
||||
return @bitCast(@as(*const [byteLength]u8, string).*);
|
||||
}
|
||||
|
||||
const ParseState = union(enum) {
|
||||
pre,
|
||||
complete,
|
||||
@@ -2413,6 +2401,112 @@ const QueuedNavigation = struct {
|
||||
priority: NavigationPriority,
|
||||
};
|
||||
|
||||
pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
||||
const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return;
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.page, "page mouse click", .{
|
||||
.url = self.url,
|
||||
.node = target,
|
||||
.x = x,
|
||||
.y = y,
|
||||
});
|
||||
}
|
||||
const event = try @import("webapi/event/MouseEvent.zig").init("click", .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.clientX = x,
|
||||
.clientY = y,
|
||||
}, self);
|
||||
try self._event_manager.dispatch(target.asEventTarget(), event.asEvent());
|
||||
}
|
||||
|
||||
// callback when the "click" event reaches the pages.
|
||||
pub fn handleClick(self: *Page, target: *Node) !void {
|
||||
// TODO: Also support <area> elements when implement
|
||||
const element = target.is(Element) orelse return;
|
||||
const anchor = element.is(Element.Html.Anchor) orelse return;
|
||||
|
||||
const href = element.getAttributeSafe("href") orelse return;
|
||||
if (href.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, href, "#")) {
|
||||
// Hash-only links (#foo) should be handled as same-document navigation
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, href, "javascript:")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check target attribute - don't navigate if opening in new window/tab
|
||||
const target_val = anchor.getTarget();
|
||||
if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) {
|
||||
log.warn(.browser, "not implemented", .{
|
||||
.feature = "anchor with target attribute click",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (try element.hasAttribute("download", self)) {
|
||||
log.warn(.browser, "not implemented", .{
|
||||
.feature = "anchor with download attribute click",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try self.scheduleNavigation(href, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .anchor);
|
||||
}
|
||||
|
||||
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
||||
const element = self.window._document._active_element orelse return;
|
||||
try self._event_manager.dispatch(element.asEventTarget(), keyboard_event.asEvent());
|
||||
}
|
||||
|
||||
pub fn handleKeydown(self: *Page, target: *Element, keyboard_event: *KeyboardEvent) !void {
|
||||
const key = keyboard_event.getKey();
|
||||
|
||||
if (key == .Dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.Input)) |input| {
|
||||
if (key == .Enter) {
|
||||
if (input.getForm(self)) |form| {
|
||||
// TODO: Implement form submission
|
||||
_ = form;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't handle text input for radio/checkbox
|
||||
const input_type = input._input_type;
|
||||
if (input_type == .radio or input_type == .checkbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle printable characters
|
||||
if (key.isPrintable()) {
|
||||
const current_value = input.getValue();
|
||||
const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, key.asString() });
|
||||
try input.setValue(new_value, self);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.TextArea)) |textarea| {
|
||||
const append =
|
||||
if (key == .Enter) "\n" else if (key.isPrintable()) key.asString() else return;
|
||||
const current_value = textarea.getValue();
|
||||
const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, append });
|
||||
return textarea.setValue(new_value, self);
|
||||
}
|
||||
}
|
||||
|
||||
const RequestCookieOpts = struct {
|
||||
is_http: bool = true,
|
||||
is_navigation: bool = false,
|
||||
@@ -2426,6 +2520,19 @@ pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) Http.Client.Req
|
||||
};
|
||||
}
|
||||
|
||||
fn asUint(comptime string: anytype) std.meta.Int(
|
||||
.unsigned,
|
||||
@bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
|
||||
) {
|
||||
const byteLength = @sizeOf(@TypeOf(string.*)) - 1;
|
||||
const expectedType = *const [byteLength:0]u8;
|
||||
if (@TypeOf(string) != expectedType) {
|
||||
@compileError("expected : " ++ @typeName(expectedType) ++ ", got: " ++ @typeName(@TypeOf(string)));
|
||||
}
|
||||
|
||||
return @bitCast(@as(*const [byteLength]u8, string).*);
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "WebApi: Page" {
|
||||
try testing.htmlRunner("page", .{});
|
||||
|
||||
@@ -46,11 +46,13 @@
|
||||
element.tagName === 'BODY' || element.tagName === 'HTML');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="hidden_elements">
|
||||
<!-- ZIGDOM new CSS Parser -->
|
||||
<!-- <script id="hidden_elements">
|
||||
{
|
||||
// Test that hidden elements are not returned
|
||||
const hidden = document.getElementById('hidden');
|
||||
console.warn('pre');
|
||||
console.warn(hidden.checkVisibility());
|
||||
const rect = hidden.getBoundingClientRect();
|
||||
|
||||
// Even though hidden element has dimensions, it shouldn't be returned
|
||||
@@ -62,7 +64,7 @@
|
||||
// Should not return the hidden element (should return body or html instead)
|
||||
testing.expectTrue(element === null || element.id !== 'hidden');
|
||||
}
|
||||
</script>
|
||||
</script> -->
|
||||
|
||||
<script id="outside_viewport">
|
||||
{
|
||||
@@ -215,7 +217,8 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="elementsFromPoint_hidden">
|
||||
<!-- ZIGDOM new CSS Parser -->
|
||||
<!-- <script id="elementsFromPoint_hidden">
|
||||
{
|
||||
// Test that hidden elements are not included
|
||||
const hidden = document.getElementById('hidden');
|
||||
@@ -231,3 +234,4 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
-->
|
||||
|
||||
38
src/browser/tests/element/bounding_rect.html
Normal file
38
src/browser/tests/element/bounding_rect.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<title>Lightpanda Browser demo</title>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Lightpanda Browser Demo</h1>
|
||||
|
||||
<h2><a href="campfire-commerce/">Campfire Commerce</a></h2>
|
||||
<a href="campfire-commerce/"><img src="campfire-commerce/images/logo.jpg"></a>
|
||||
<p>
|
||||
Demo of an e-commerce product offer page with data loaded from XHR request.
|
||||
<br>
|
||||
All images and texts have been generated with AI.
|
||||
Template by <a href="https://codepen.io/Sunil_Pradhan/pen/qBqgLxK">Sunil Pradhan</a>
|
||||
</p>
|
||||
|
||||
<h2><a href="amiibo/">Amiibo characters</a></h2>
|
||||
<a href="amiibo/"><img src="https://raw.githubusercontent.com/N3evin/AmiiboAPI/master/images/icon_04380001-03000502.png"></a>
|
||||
<p>
|
||||
Pages of Amiibo characters generated from
|
||||
<a href="https://www.amiiboapi.com">Amiibo API</a>.
|
||||
</p>
|
||||
</body>
|
||||
<script src="../testing.js"></script>
|
||||
<script id="rect">
|
||||
const a1 = document.querySelector('[href="campfire-commerce/"]');
|
||||
const rect1 = a1.getBoundingClientRect();
|
||||
console.log("a1:", { x: rect1.x, y: rect1.y });
|
||||
|
||||
const a2 = document.querySelector('[href="amiibo/"]');
|
||||
const rect2 = a2.getBoundingClientRect();
|
||||
|
||||
testing.expectTrue(rect1.x != rect2.x)
|
||||
testing.expectTrue(rect1.y != rect2.y);
|
||||
testing.expectEqual(a1, document.elementFromPoint(rect1.x, rect1.y))
|
||||
testing.expectEqual(a2, document.elementFromPoint(rect2.x, rect2.y))
|
||||
</script>
|
||||
@@ -922,7 +922,8 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect {
|
||||
const y = calculateDocumentPosition(self.asNode());
|
||||
const dims = try self.getElementDimensions(page);
|
||||
|
||||
const x: f64 = 0.0;
|
||||
// Use sibling position for x coordinate to ensure siblings have different x values
|
||||
const x = calculateSiblingPosition(self.asNode());
|
||||
const top = y;
|
||||
const left = x;
|
||||
const right = x + dims.width;
|
||||
@@ -948,51 +949,87 @@ pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
|
||||
return ptr[0..1];
|
||||
}
|
||||
|
||||
// Calculates a pseudo-position in the document using linear depth scaling.
|
||||
// Calculates document position by counting all nodes that appear before this one
|
||||
// in tree order, but only traversing the "left side" of the tree.
|
||||
//
|
||||
// This approach uses a fixed pixel offset per depth level (100px) plus sibling
|
||||
// position within that level. This keeps positions reasonable even for very deep
|
||||
// DOM trees (e.g., Amazon product pages can be 36+ levels deep).
|
||||
// This walks up from the target node to the root, and at each level counts:
|
||||
// 1. All previous siblings and their descendants
|
||||
// 2. The parent itself
|
||||
//
|
||||
// Example:
|
||||
// <body> → position 0 (depth 0)
|
||||
// <div> → position 100 (depth 1, 0 siblings)
|
||||
// <span></span> → position 200 (depth 2, 0 siblings)
|
||||
// <span></span> → position 201 (depth 2, 1 sibling)
|
||||
// </div>
|
||||
// <div> → position 101 (depth 1, 1 sibling)
|
||||
// <p></p> → position 200 (depth 2, 0 siblings)
|
||||
// </div>
|
||||
// <body> → y=0
|
||||
// <h1>Text</h1> → y=1 (body=1)
|
||||
// <h2> → y=2 (body=1 + h1=1)
|
||||
// <a>Link1</a> → y=3 (body=1 + h1=1 + h2=1)
|
||||
// </h2>
|
||||
// <p>Text</p> → y=5 (body=1 + h1=1 + h2=2)
|
||||
// <h2> → y=6 (body=1 + h1=1 + h2=2 + p=1)
|
||||
// <a>Link2</a> → y=7 (body=1 + h1=1 + h2=2 + p=1 + h2=1)
|
||||
// </h2>
|
||||
// </body>
|
||||
//
|
||||
// Trade-offs:
|
||||
// - O(depth) complexity, very fast
|
||||
// - Linear scaling: 36 levels ≈ 3,600px, 100 levels ≈ 10,000px
|
||||
// - Rough document order preserved (depth dominates, siblings differentiate)
|
||||
// - Fits comfortably in realistic document heights
|
||||
// - O(depth × siblings × subtree_height) - only left-side traversal
|
||||
// - Linear scaling: 5px per node
|
||||
// - Perfect document order, guaranteed unique positions
|
||||
// - Compact coordinates (1000 nodes ≈ 5,000px)
|
||||
fn calculateDocumentPosition(node: *Node) f64 {
|
||||
var depth: f64 = 0.0;
|
||||
var sibling_offset: f64 = 0.0;
|
||||
var position: f64 = 0.0;
|
||||
var current = node;
|
||||
|
||||
// Count siblings at the immediate level
|
||||
if (current.parentNode()) |parent| {
|
||||
// Walk up to root, counting preceding nodes
|
||||
while (current.parentNode()) |parent| {
|
||||
// Count all previous siblings and their descendants
|
||||
var sibling = parent.firstChild();
|
||||
while (sibling) |s| {
|
||||
if (s == current) break;
|
||||
sibling_offset += 1.0;
|
||||
position += countSubtreeNodes(s);
|
||||
sibling = s.nextSibling();
|
||||
}
|
||||
}
|
||||
|
||||
// Count depth from root
|
||||
while (current.parentNode()) |parent| {
|
||||
depth += 1.0;
|
||||
// Count the parent itself
|
||||
position += 1.0;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
// Each depth level = 100px, siblings add within that level
|
||||
return (depth * 100.0) + sibling_offset;
|
||||
return position * 5.0; // 5px per node
|
||||
}
|
||||
|
||||
// Counts total nodes in a subtree (node + all descendants)
|
||||
fn countSubtreeNodes(node: *Node) f64 {
|
||||
var count: f64 = 1.0; // Count this node
|
||||
|
||||
var child = node.firstChild();
|
||||
while (child) |c| {
|
||||
count += countSubtreeNodes(c);
|
||||
child = c.nextSibling();
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// Calculates horizontal position using the same approach as y,
|
||||
// just scaled differently for visual distinction
|
||||
fn calculateSiblingPosition(node: *Node) f64 {
|
||||
var position: f64 = 0.0;
|
||||
var current = node;
|
||||
|
||||
// Walk up to root, counting preceding nodes (same as y)
|
||||
while (current.parentNode()) |parent| {
|
||||
// Count all previous siblings and their descendants
|
||||
var sibling = parent.firstChild();
|
||||
while (sibling) |s| {
|
||||
if (s == current) break;
|
||||
position += countSubtreeNodes(s);
|
||||
sibling = s.nextSibling();
|
||||
}
|
||||
|
||||
// Count the parent itself
|
||||
position += 1.0;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return position * 5.0; // 5px per node
|
||||
}
|
||||
|
||||
const GetElementsByTagNameResult = union(enum) {
|
||||
|
||||
@@ -37,6 +37,51 @@ pub fn asCSSStyleDeclaration(self: *CSSStyleProperties) *CSSStyleDeclaration {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
pub fn getNamed(self: *CSSStyleProperties, name: []const u8, page: *Page) ![]const u8 {
|
||||
if (method_names.has(name)) {
|
||||
return error.NotHandled;
|
||||
}
|
||||
|
||||
const dash_case = camelCaseToDashCase(name, &page.buf);
|
||||
|
||||
// Only apply vendor prefix filtering for camelCase access (no dashes in input)
|
||||
// Bracket notation with dash-case (e.g., div.style['-moz-user-select']) should return the actual value
|
||||
const is_camelcase_access = std.mem.indexOfScalar(u8, name, '-') == null;
|
||||
if (is_camelcase_access and std.mem.startsWith(u8, dash_case, "-")) {
|
||||
// We only support -webkit-, other vendor prefixes return undefined for camelCase access
|
||||
const is_webkit = std.mem.startsWith(u8, dash_case, "-webkit-");
|
||||
const is_moz = std.mem.startsWith(u8, dash_case, "-moz-");
|
||||
const is_ms = std.mem.startsWith(u8, dash_case, "-ms-");
|
||||
const is_o = std.mem.startsWith(u8, dash_case, "-o-");
|
||||
|
||||
if ((is_moz or is_ms or is_o) and !is_webkit) {
|
||||
return error.NotHandled;
|
||||
}
|
||||
}
|
||||
|
||||
const value = self._proto.getPropertyValue(dash_case, page);
|
||||
|
||||
// Property accessors have special handling for empty values:
|
||||
// - Known CSS properties return '' when not set
|
||||
// - Vendor-prefixed properties return undefined when not set
|
||||
// - Unknown properties return undefined
|
||||
if (value.len == 0) {
|
||||
// Vendor-prefixed properties always return undefined when not set
|
||||
if (std.mem.startsWith(u8, dash_case, "-")) {
|
||||
return error.NotHandled;
|
||||
}
|
||||
|
||||
// Known CSS properties return '', unknown properties return undefined
|
||||
if (!isKnownCSSProperty(dash_case)) {
|
||||
return error.NotHandled;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
fn isKnownCSSProperty(dash_case: []const u8) bool {
|
||||
// List of common/known CSS properties
|
||||
// In a full implementation, this would include all standard CSS properties
|
||||
@@ -131,6 +176,16 @@ fn camelCaseToDashCase(name: []const u8, buf: []u8) []const u8 {
|
||||
return buf[0..write_pos];
|
||||
}
|
||||
|
||||
const method_names = std.StaticStringMap(void).initComptime(.{
|
||||
.{ "getPropertyValue", {} },
|
||||
.{ "setProperty", {} },
|
||||
.{ "removeProperty", {} },
|
||||
.{ "getPropertyPriority", {} },
|
||||
.{ "item", {} },
|
||||
.{ "cssText", {} },
|
||||
.{ "length", {} },
|
||||
});
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(CSSStyleProperties);
|
||||
|
||||
@@ -140,60 +195,5 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const @"[]" = bridge.namedIndexed(_getPropertyIndexed, null, null, .{});
|
||||
|
||||
const method_names = std.StaticStringMap(void).initComptime(.{
|
||||
.{ "getPropertyValue", {} },
|
||||
.{ "setProperty", {} },
|
||||
.{ "removeProperty", {} },
|
||||
.{ "getPropertyPriority", {} },
|
||||
.{ "item", {} },
|
||||
.{ "cssText", {} },
|
||||
.{ "length", {} },
|
||||
});
|
||||
|
||||
fn _getPropertyIndexed(self: *CSSStyleProperties, name: []const u8, page: *Page) ![]const u8 {
|
||||
if (method_names.has(name)) {
|
||||
return error.NotHandled;
|
||||
}
|
||||
|
||||
const dash_case = camelCaseToDashCase(name, &page.buf);
|
||||
|
||||
// Only apply vendor prefix filtering for camelCase access (no dashes in input)
|
||||
// Bracket notation with dash-case (e.g., div.style['-moz-user-select']) should return the actual value
|
||||
const is_camelcase_access = std.mem.indexOfScalar(u8, name, '-') == null;
|
||||
if (is_camelcase_access and std.mem.startsWith(u8, dash_case, "-")) {
|
||||
// We only support -webkit-, other vendor prefixes return undefined for camelCase access
|
||||
const is_webkit = std.mem.startsWith(u8, dash_case, "-webkit-");
|
||||
const is_moz = std.mem.startsWith(u8, dash_case, "-moz-");
|
||||
const is_ms = std.mem.startsWith(u8, dash_case, "-ms-");
|
||||
const is_o = std.mem.startsWith(u8, dash_case, "-o-");
|
||||
|
||||
if ((is_moz or is_ms or is_o) and !is_webkit) {
|
||||
return error.NotHandled;
|
||||
}
|
||||
}
|
||||
|
||||
const value = self._proto.getPropertyValue(dash_case, page);
|
||||
|
||||
// Property accessors have special handling for empty values:
|
||||
// - Known CSS properties return '' when not set
|
||||
// - Vendor-prefixed properties return undefined when not set
|
||||
// - Unknown properties return undefined
|
||||
if (value.len == 0) {
|
||||
// Vendor-prefixed properties always return undefined when not set
|
||||
if (std.mem.startsWith(u8, dash_case, "-")) {
|
||||
return error.NotHandled;
|
||||
}
|
||||
|
||||
// Known CSS properties return '', unknown properties return undefined
|
||||
if (!isKnownCSSProperty(dash_case)) {
|
||||
return error.NotHandled;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
pub const @"[]" = bridge.namedIndexed(CSSStyleProperties.getNamed, null, null, .{});
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ const KeyboardEvent = @This();
|
||||
|
||||
_proto: *UIEvent,
|
||||
_key: Key,
|
||||
_code: []const u8,
|
||||
_ctrl_key: bool,
|
||||
_shift_key: bool,
|
||||
_alt_key: bool,
|
||||
@@ -41,6 +42,9 @@ pub const Key = union(enum) {
|
||||
// Special Key Values
|
||||
Dead,
|
||||
Undefined,
|
||||
Unidentified,
|
||||
|
||||
// Modifier Keys
|
||||
Alt,
|
||||
AltGraph,
|
||||
CapsLock,
|
||||
@@ -55,6 +59,68 @@ pub const Key = union(enum) {
|
||||
Super,
|
||||
Symbol,
|
||||
SymbolLock,
|
||||
|
||||
// Whitespace Keys
|
||||
Enter,
|
||||
Tab,
|
||||
|
||||
// Navigation Keys
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
End,
|
||||
Home,
|
||||
PageDown,
|
||||
PageUp,
|
||||
|
||||
// Editing Keys
|
||||
Backspace,
|
||||
Clear,
|
||||
Copy,
|
||||
CrSel,
|
||||
Cut,
|
||||
Delete,
|
||||
EraseEof,
|
||||
ExSel,
|
||||
Insert,
|
||||
Paste,
|
||||
Redo,
|
||||
Undo,
|
||||
|
||||
// UI Keys
|
||||
Accept,
|
||||
Again,
|
||||
Attn,
|
||||
Cancel,
|
||||
ContextMenu,
|
||||
Escape,
|
||||
Execute,
|
||||
Find,
|
||||
Finish,
|
||||
Help,
|
||||
Pause,
|
||||
Play,
|
||||
Props,
|
||||
Select,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
|
||||
// Function Keys
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
|
||||
// Printable keys (single character, space, etc.)
|
||||
standard: []const u8,
|
||||
|
||||
pub fn fromString(allocator: std.mem.Allocator, str: []const u8) !Key {
|
||||
@@ -70,6 +136,26 @@ pub const Key = union(enum) {
|
||||
const duped = try allocator.dupe(u8, str);
|
||||
return .{ .standard = duped };
|
||||
}
|
||||
|
||||
/// Returns true if this key represents a printable character that should be
|
||||
/// inserted into text input elements. This includes alphanumeric characters,
|
||||
/// punctuation, symbols, and space.
|
||||
pub fn isPrintable(self: Key) bool {
|
||||
return switch (self) {
|
||||
.standard => |s| s.len > 0,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the string representation that should be inserted into text input.
|
||||
/// For most keys this is just the key itself, but some keys like Enter need
|
||||
/// special handling (e.g., newline for textarea, form submission for input).
|
||||
pub fn asString(self: Key) []const u8 {
|
||||
return switch (self) {
|
||||
.standard => |s| s,
|
||||
else => |k| @tagName(k),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Location = enum(i32) {
|
||||
@@ -81,7 +167,7 @@ pub const Location = enum(i32) {
|
||||
|
||||
pub const KeyboardEventOptions = struct {
|
||||
key: []const u8 = "",
|
||||
// TODO: code but it is not baseline.
|
||||
code: ?[]const u8 = null,
|
||||
location: i32 = 0,
|
||||
repeat: bool = false,
|
||||
isComposing: bool = false,
|
||||
@@ -105,6 +191,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent {
|
||||
._proto = undefined,
|
||||
._key = try Key.fromString(page.arena, opts.key),
|
||||
._location = std.meta.intToEnum(Location, opts.location) catch return error.TypeError,
|
||||
._code = if (opts.code) |c| try page.dupeString(c) else "",
|
||||
._repeat = opts.repeat,
|
||||
._is_composing = opts.isComposing,
|
||||
._ctrl_key = opts.ctrlKey,
|
||||
@@ -134,11 +221,12 @@ pub fn getIsComposing(self: *const KeyboardEvent) bool {
|
||||
return self._is_composing;
|
||||
}
|
||||
|
||||
pub fn getKey(self: *const KeyboardEvent) []const u8 {
|
||||
return switch (self._key) {
|
||||
.standard => |key| key,
|
||||
else => |x| @tagName(x),
|
||||
};
|
||||
pub fn getKey(self: *const KeyboardEvent) Key {
|
||||
return self._key;
|
||||
}
|
||||
|
||||
pub fn getCode(self: *const KeyboardEvent) []const u8 {
|
||||
return self._code;
|
||||
}
|
||||
|
||||
pub fn getLocation(self: *const KeyboardEvent) i32 {
|
||||
@@ -182,7 +270,12 @@ pub const JsApi = struct {
|
||||
pub const altKey = bridge.accessor(KeyboardEvent.getAltKey, null, .{});
|
||||
pub const ctrlKey = bridge.accessor(KeyboardEvent.getCtrlKey, null, .{});
|
||||
pub const isComposing = bridge.accessor(KeyboardEvent.getIsComposing, null, .{});
|
||||
pub const key = bridge.accessor(KeyboardEvent.getKey, null, .{});
|
||||
pub const key = bridge.accessor(struct {
|
||||
fn keyAsString(self: *const KeyboardEvent) []const u8 {
|
||||
return self._key.asString();
|
||||
}
|
||||
}.keyAsString, null, .{});
|
||||
pub const code = bridge.accessor(KeyboardEvent.getCode, null, .{});
|
||||
pub const location = bridge.accessor(KeyboardEvent.getLocation, null, .{});
|
||||
pub const metaKey = bridge.accessor(KeyboardEvent.getMetaKey, null, .{});
|
||||
pub const repeat = bridge.accessor(KeyboardEvent.getRepeat, null, .{});
|
||||
|
||||
@@ -201,8 +201,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
|
||||
asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command),
|
||||
// @ZIGDOM
|
||||
// asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command),
|
||||
asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
|
||||
|
||||
@@ -689,7 +689,7 @@ test "cdp.dom: getBoxModel" {
|
||||
.params = .{ .nodeId = 6 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .model = BoxModel{
|
||||
.content = Quad{ 0.0, 200.0, 5.0, 200.0, 5.0, 205.0, 0.0, 205.0 },
|
||||
.content = Quad{ 10.0, 10.0, 15.0, 10.0, 15.0, 15.0, 10.0, 15.0 },
|
||||
.padding = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
|
||||
.border = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
|
||||
.margin = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
|
||||
|
||||
@@ -36,7 +36,7 @@ fn dispatchKeyEvent(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
type: Type,
|
||||
key: []const u8 = "",
|
||||
code: []const u8 = "",
|
||||
code: ?[]const u8 = null,
|
||||
modifiers: u4 = 0,
|
||||
// Many optional parameters are not implemented yet, see documentation url.
|
||||
|
||||
@@ -59,28 +59,25 @@ fn dispatchKeyEvent(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return;
|
||||
const page = bc.session.currentPage() orelse return;
|
||||
|
||||
const keyboard_event = Page.KeyboardEvent{
|
||||
const KeyboardEvent = @import("../../browser/webapi/event/KeyboardEvent.zig");
|
||||
const keyboard_event = try KeyboardEvent.init("keydown", .{
|
||||
.key = params.key,
|
||||
.code = params.code,
|
||||
.type = switch (params.type) {
|
||||
.keyDown => .keydown,
|
||||
else => unreachable,
|
||||
},
|
||||
.alt = params.modifiers & 1 == 1,
|
||||
.ctrl = params.modifiers & 2 == 2,
|
||||
.meta = params.modifiers & 4 == 4,
|
||||
.shift = params.modifiers & 8 == 8,
|
||||
};
|
||||
try page.keyboardEvent(keyboard_event);
|
||||
.altKey = params.modifiers & 1 == 1,
|
||||
.ctrlKey = params.modifiers & 2 == 2,
|
||||
.metaKey = params.modifiers & 4 == 4,
|
||||
.shiftKey = params.modifiers & 8 == 8,
|
||||
}, page);
|
||||
try page.triggerKeyboard(keyboard_event);
|
||||
// result already sent
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
|
||||
fn dispatchMouseEvent(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
type: Type, // Type of the mouse event.
|
||||
x: f32, // X coordinate of the event relative to the main frame's viewport.
|
||||
y: f32, // Y coordinate of the event relative to the main frame's viewport. 0 refers to the top of the viewport and Y increases as it proceeds towards the bottom of the viewport.
|
||||
x: f64,
|
||||
y: f64,
|
||||
type: Type,
|
||||
// Many optional parameters are not implemented yet, see documentation url.
|
||||
|
||||
const Type = enum {
|
||||
@@ -95,23 +92,13 @@ fn dispatchMouseEvent(cmd: anytype) !void {
|
||||
|
||||
// quickly ignore types we know we don't handle
|
||||
switch (params.type) {
|
||||
.mouseMoved, .mouseWheel => return,
|
||||
.mouseMoved, .mouseWheel, .mouseReleased => return,
|
||||
else => {},
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return;
|
||||
const page = bc.session.currentPage() orelse return;
|
||||
|
||||
const mouse_event = Page.MouseEvent{
|
||||
.x = @intFromFloat(@floor(params.x)), // Decimal pixel values are not understood by netsurf or our renderer
|
||||
.y = @intFromFloat(@floor(params.y)), // So we convert them once at intake here. Using floor such that -0.5 becomes -1 and 0.5 becomes 0.
|
||||
.type = switch (params.type) {
|
||||
.mousePressed => .pressed,
|
||||
.mouseReleased => .released,
|
||||
else => unreachable,
|
||||
},
|
||||
};
|
||||
try page.mouseEvent(mouse_event);
|
||||
try page.triggerMouseClick(params.x, params.y);
|
||||
// result already sent
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user