summaryrefslogtreecommitdiff
path: root/src/pdf_pane_integration_test.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-06 18:11:36 -0300
committerGabriel Schneider <[email protected]>2026-09-07 13:59:12 -0300
commit60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch)
tree310fc734173cf771881f4691c71909135fadde97 /src/pdf_pane_integration_test.zig
parentfa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff)
downloadpardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz
pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip
Refactor panes and filesystem; replace FUSE with 9P
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples. Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
Diffstat (limited to 'src/pdf_pane_integration_test.zig')
-rw-r--r--src/pdf_pane_integration_test.zig1819
1 files changed, 0 insertions, 1819 deletions
diff --git a/src/pdf_pane_integration_test.zig b/src/pdf_pane_integration_test.zig
deleted file mode 100644
index f0a8be8f..00000000
--- a/src/pdf_pane_integration_test.zig
+++ /dev/null
@@ -1,1819 +0,0 @@
-//! End-to-end PDF pane tests. Production state and behavior stay in pdf_pane.zig;
-//! this module exercises their direct seam with Pardes layout, input, and output.
-const std = @import("std");
-const pardes = @import("pardes.zig");
-const pdf_pane = @import("pdf_pane.zig");
-const file_pane = @import("file_pane.zig");
-const look = @import("look.zig");
-const pdf_impl = pdf_pane.pdf;
-const image = @import("image.zig");
-const config = @import("config.zig");
-const builtins = @import("builtins.zig");
-const animation = pardes.animation;
-const dump = pardes.dump;
-
-const Pardes = pardes.Pardes;
-const Builtin = builtins.registry.Builtin();
-const Key = pardes.Key;
-const Event = pardes.Event;
-const Mode = pardes.Mode;
-const Surface = pardes.Surface;
-const ImagePlace = pardes.ImagePlace;
-const PdfFitMode = pdf_pane.FitMode;
-const PdfTintMode = pdf_pane.TintMode;
-const pdf_enabled = pdf_pane.enabled;
-const platform = pardes.platform;
-const themes = pardes.themes;
-const pdf_raster_policy = pardes.pdf_raster_policy;
-const PDF_PAGE_GAP_PX = pardes.PDF_PAGE_GAP_PX;
-const BOX_H = pardes.BOX_H;
-const sel_slot = @intFromEnum(config.select_button);
-const pane_tail = " " ++ config.pane_builtins_str;
-
-fn hasPdf(pane: *const pardes.Pane) bool {
- return if (comptime pdf_enabled) pane.pdf != null else false;
-}
-
-test "PDF feature gates keep argv and builtin behavior coherent" {
- const maybe_fit = std.meta.stringToEnum(Builtin, "PdfFit");
- const maybe_tint = std.meta.stringToEnum(Builtin, "PdfTint");
- const maybe_sections = std.meta.stringToEnum(Builtin, "PdfSections");
- try std.testing.expectEqual(pdf_enabled, maybe_fit != null);
- try std.testing.expectEqual(pdf_enabled, maybe_tint != null);
- try std.testing.expectEqual(pdf_enabled, maybe_sections != null);
- if (pdf_enabled) {
- try std.testing.expectEqualStrings("tz", config.leader_path.get(maybe_fit.?).?);
- try std.testing.expectEqualStrings("ti", config.leader_path.get(maybe_tint.?).?);
- try std.testing.expectEqualStrings("ts", config.leader_path.get(maybe_sections.?).?);
- } else if (platform != .web) {
- const p = try Pardes.init(std.testing.allocator, .{
- .file = "docs/design.pdf",
- .cols = 80,
- .rows = 24,
- });
- defer p.deinit();
- const pane = p.panes[0].?;
- try std.testing.expect(pane.file != null);
- try std.testing.expect(pane.image == null);
- try std.testing.expect(!hasPdf(pane));
- }
-}
-
-test "PDF dump fallback remains a byte-preserving file" {
- const gpa = std.testing.allocator;
- const path = "/definitely/missing/pardes-dump-fallback.PDF";
- const source = "%PDF embedded fallback bytes\x00\xff";
- const encoded = try dump.encodeBytes(gpa, source);
- defer gpa.free(encoded);
- const ids = [_]usize{0};
- const columns = [_]dump.Column{.{ .panes = &ids }};
- const panes = [_]dump.Pane{.{
- .kind = .image,
- .tag = "pdf 3/9 height PdfFit full PdfTint PdfSections " ++ path ++ " Keep Del",
- .body = "",
- .scroll = 2,
- .cols = 80,
- .rows = 24,
- .image = .{ .path = path, .bytes_b64 = encoded },
- }};
- const state = dump.State{
- .screen = .{ .cols = 80, .rows = 24 },
- .columns = &columns,
- .panes = &panes,
- };
- var out: std.Io.Writer.Allocating = .init(gpa);
- defer out.deinit();
- try std.zon.stringify.serialize(state, .{ .whitespace = true }, &out.writer);
-
- const restored = try Pardes.initFromDump(gpa, .{ .tty_only = true }, out.written());
- defer restored.deinit();
- const pane = restored.panes[0].?;
- try std.testing.expect(pane.file != null);
- try std.testing.expect(pane.image == null);
- try std.testing.expect(!hasPdf(pane));
- try std.testing.expectEqualStrings(path, pane.file.?.path);
- try std.testing.expectEqualSlices(u8, source, pane.file.?.content);
- try std.testing.expect(pane.tag_init);
- try std.testing.expectEqualStrings(" Keep Del", pane.tag_tail[0..pane.tag_tail_len]);
-}
-
-test "PdfSections Look follows the exact owning PDF, not an equal path" {
- if (!pdf_enabled or platform == .web) return;
- const gpa = std.testing.allocator;
- var tmp = std.testing.tmpDir(.{});
- defer tmp.cleanup();
- const fixture = try pdf_impl.makeOutlineTestPdf(gpa);
- defer gpa.free(fixture);
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "outline.pdf", .data = fixture });
- var path_buf: [256]u8 = undefined;
- const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/outline.pdf", .{tmp.sub_path});
-
- const p = try Pardes.init(gpa, .{ .file = path, .cols = 80, .rows = 28 });
- defer p.deinit();
- const first = p.panes[0].?;
- // Bare normal-mode `f` on a PDF dispatches the PdfSections builtin.
- p.update(.{ .key = .{ .cp = 'f', .text = "f" } });
- const first_output_id = first.search_pane orelse return error.MissingPdfSectionsOutput;
- const first_output = p.panes[first_output_id].?;
- try std.testing.expect(pdf_pane.isSectionsOutput(first_output));
- try std.testing.expect(first_output.file.?.content.len > 0);
-
- // A clean result is re-armed in place without rebuilding it.
- const first_revision = first_output.file.?.revision;
- pdf_pane.openSections(p, 0);
- try std.testing.expectEqual(first_output_id, first.search_pane.?);
- try std.testing.expectEqual(first_revision, first_output.file.?.revision);
-
- const duplicate_id = p.freeSlot() orelse return error.NoDuplicatePdfSlot;
- const duplicate = try pdf_pane.openPane(p, duplicate_id, path, 0);
- p.placeDoc(0, duplicate_id, duplicate);
- p.computeGeom();
- pdf_pane.openSections(p, duplicate_id);
- const duplicate_output_id = duplicate.search_pane orelse return error.MissingDuplicatePdfSections;
-
- const first_row = std.mem.sliceTo(first_output.file.?.content, '\n');
- const target = first_row[0 .. std.mem.indexOfScalar(u8, first_row, ' ') orelse first_row.len];
- p.lookAt(first_output_id, target);
- try std.testing.expectEqual(@as(usize, 0), p.active);
- p.lookAt(duplicate_output_id, target);
- try std.testing.expectEqual(duplicate_id, p.active);
-
- // Once the original document is gone, its output cannot reinterpret an
- // old ordinal against the still-open equal-path duplicate.
- try std.testing.expect(pardes.pdf_test.runBuiltin(p, "Del", 0, "", null));
- p.active = first_output_id;
- p.lookAt(first_output_id, target);
- try std.testing.expectEqual(first_output_id, p.active);
-}
-
-test "PdfSections caches an empty outline output" {
- if (!pdf_enabled or platform == .web) return;
- const gpa = std.testing.allocator;
- var tmp = std.testing.tmpDir(.{});
- defer tmp.cleanup();
- const fixture = try pdf_impl.makeNoOutlineTestPdf(gpa);
- defer gpa.free(fixture);
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "plain.pdf", .data = fixture });
- var path_buf: [256]u8 = undefined;
- const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/plain.pdf", .{tmp.sub_path});
- const p = try Pardes.init(gpa, .{ .file = path });
- defer p.deinit();
- pdf_pane.openSections(p, 0);
- const pane = p.panes[0].?;
- const output_id = pane.search_pane orelse return error.MissingEmptyPdfSections;
- try std.testing.expectEqual(@as(usize, 0), p.panes[output_id].?.file.?.content.len);
- const serial = p.panes[output_id].?.serial;
- pdf_pane.openSections(p, 0);
- try std.testing.expectEqual(output_id, pane.search_pane.?);
- try std.testing.expectEqual(serial, p.panes[output_id].?.serial);
-}
-
-test "SDL PDF raster policy is materially denser than Kitty" {
- try std.testing.expect(
- pardes.sdl_pdf_raster_policy.dpi >= pardes.kitty_pdf_raster_policy.dpi * 2,
- );
- try std.testing.expect(
- pardes.sdl_pdf_raster_policy.max_dimension >
- pardes.kitty_pdf_raster_policy.max_dimension * 3,
- );
- try std.testing.expect(!pardes.kitty_pdf_raster_policy.match_viewport);
- try std.testing.expect(pardes.sdl_pdf_raster_policy.match_viewport);
-}
-
-test "watched PDF reload replaces MuPDF state and preserves the reading view" {
- if (!pdf_enabled or platform == .web) return;
-
- const gpa = std.testing.allocator;
- var tmp = std.testing.tmpDir(.{});
- defer tmp.cleanup();
- const original = try pdf_impl.makeOutlineTestPdf(gpa);
- defer gpa.free(original);
- const replacement = try pdf_impl.makeNoOutlineTestPdf(gpa);
- defer gpa.free(replacement);
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "live.pdf", .data = original });
- var path_buf: [256]u8 = undefined;
- const path = try std.fmt.bufPrint(
- &path_buf,
- ".zig-cache/tmp/{s}/live.pdf",
- .{tmp.sub_path},
- );
-
- const p = try Pardes.init(gpa, .{ .file = path, .cols = 80, .rows = 28 });
- defer p.deinit();
- var watched = false;
- while (p.nextEffect()) |effect| switch (effect) {
- .watch => |watch| if (watch.pane == 0 and watch.on) {
- watched = true;
- },
- else => {},
- };
- try std.testing.expect(watched);
-
- const pane = p.panes[0].?;
- const state = &pane.pdf.?;
- try std.testing.expectEqual(@as(usize, 3), state.page_count);
- pdf_pane.openSections(p, 0);
- const sections_id = pane.search_pane orelse return error.MissingPdfSectionsOutput;
- const sections = p.panes[sections_id].?;
- try std.testing.expect(sections.file.?.content.len > 0);
-
- p.native_images = true;
- state.fit = .height;
- state.tint = .full;
- state.pan_x = 1234;
- state.pan_y = 4321;
- try state.setSearchQuery(p.pdf_gpa, "needle");
- pdf_pane.setPage(p, pane, 2);
- var frame = std.heap.ArenaAllocator.init(gpa);
- defer frame.deinit();
- _ = try p.render(frame.allocator());
- state.text_scroll = 7;
- state.search_hit = 4;
- state.search_reveal_pending = false;
- const reveal_viewport_w = state.reveal_viewport_w;
- const reveal_viewport_h = state.reveal_viewport_h;
- const reveal_fit = state.reveal_fit;
- const reveal_viewport_valid = state.reveal_viewport_valid;
- const old_revision = pdf_pane.rasterForPage(state, 2).?.revision;
- const old_revision_counter = state.next_raster_revision;
- const anchor_fraction: f64 = 0.375;
- state.document_scroll_y = @as(f64, @floatFromInt(state.page_starts[2])) +
- anchor_fraction * @as(f64, @floatFromInt(state.page_heights[2]));
- state.scroll_to_page_pending = false;
- state.layout_anchor_pending = false;
- pane.cur_row = 9;
- pane.cur_col = 8;
-
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "live.pdf", .data = replacement });
- try std.testing.expect(p.reloadWatchedFile(0, &.{}));
-
- try std.testing.expectEqual(@as(usize, 1), state.page_count);
- try std.testing.expectEqual(@as(usize, 0), state.page);
- try std.testing.expectEqual(PdfFitMode.height, state.fit);
- try std.testing.expectEqual(PdfTintMode.full, state.tint);
- try std.testing.expectEqual(@as(u16, 1234), state.pan_x);
- try std.testing.expectEqual(@as(u16, 4321), state.pan_y);
- try std.testing.expectEqual(@as(usize, 7), state.text_scroll);
- try std.testing.expect(state.text_scroll_clamp_pending);
- try std.testing.expectEqualStrings("needle", state.search_query);
- try std.testing.expectEqual(@as(usize, 4), state.search_hit);
- try std.testing.expectEqual(reveal_viewport_w, state.reveal_viewport_w);
- try std.testing.expectEqual(reveal_viewport_h, state.reveal_viewport_h);
- try std.testing.expectEqual(reveal_fit, state.reveal_fit);
- try std.testing.expectEqual(reveal_viewport_valid, state.reveal_viewport_valid);
- try std.testing.expectEqual(old_revision_counter, state.next_raster_revision);
- try std.testing.expectEqual(@as(usize, 0), state.rasters_len);
- try std.testing.expect(state.layout_anchor_pending);
- try std.testing.expectEqual(@as(usize, 2), state.layout_anchor_page);
- try std.testing.expectApproxEqAbs(anchor_fraction, state.layout_anchor_fraction, 0.0001);
- try std.testing.expectEqual(@as(i32, 0), pane.cur_row);
- try std.testing.expectEqual(@as(i32, 0), pane.cur_col);
- _ = state.ensureText(p.pdf_gpa);
- const text_lines = std.mem.count(u8, state.text, "\n") + 1;
- try std.testing.expect(state.text_scroll < text_lines);
- try std.testing.expect(!state.text_scroll_clamp_pending);
-
- // A clean generated outline is derived data: it refreshes in place and
- // remains the remembered output. This replacement deliberately has none.
- try std.testing.expectEqual(@as(usize, 0), sections.file.?.content.len);
- try std.testing.expectEqual(sections_id, state.sections_output.?.pane);
- try std.testing.expectEqual(sections.file.?.revision, state.sections_output.?.revision);
-
- _ = frame.reset(.retain_capacity);
- _ = try p.render(frame.allocator());
- const fresh_raster = pdf_pane.rasterForPage(state, 0).?;
- try std.testing.expect(fresh_raster.revision != old_revision);
- try std.testing.expect(fresh_raster.revision > old_revision_counter);
- const expected_scroll = anchor_fraction *
- @as(f64, @floatFromInt(state.page_heights[0]));
- const viewport = pdf_pane.paneViewport(p, pane).?;
- const max_scroll = @as(f64, @floatFromInt(state.document_height -| viewport.pixel_h));
- try std.testing.expectApproxEqAbs(@min(expected_scroll, max_scroll), state.document_scroll_y, 0.001);
-
- // Reopen is transactional: malformed replacement bytes leave the last
- // good MuPDF handle and every durable setting untouched.
- const good_handle = state.document.handle;
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "live.pdf", .data = "not a PDF" });
- try std.testing.expect(!p.reloadWatchedFile(0, &.{}));
- try std.testing.expectEqual(good_handle, state.document.handle);
- try std.testing.expectEqual(@as(usize, 1), state.page_count);
- try std.testing.expectEqualStrings("needle", state.search_query);
- try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "PDF reload") != null);
-
- // A user edit breaks the generated-revision token. A later valid reload
- // updates the document but leaves those user-owned output bytes alone.
- file_pane.setContent(p, &sections.file.?, try gpa.dupe(u8, "edited sections\n"));
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "live.pdf", .data = original });
- try std.testing.expect(p.reloadWatchedFile(0, &.{}));
- try std.testing.expectEqual(@as(usize, 3), state.page_count);
- try std.testing.expectEqualStrings("edited sections\n", sections.file.?.content);
-
- // The watch follows the PDF payload's lifetime just like a text file's;
- // emit the stop while the slot still identifies the disappearing pane.
- p.deinitPane(pane);
- p.panes[0] = null;
- var unwatched = false;
- while (p.nextEffect()) |effect| switch (effect) {
- .watch => |watch| if (watch.pane == 0 and !watch.on) {
- unwatched = true;
- },
- else => {},
- };
- try std.testing.expect(unwatched);
-}
-
-test "PDF reload does not re-center an already revealed matching search" {
- if (!pdf_enabled or platform == .web) return;
- const gpa = std.testing.allocator;
- const fixture = try look.readFile(gpa, "docs/design.pdf");
- defer gpa.free(fixture);
- var tmp = std.testing.tmpDir(.{});
- defer tmp.cleanup();
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "search.pdf", .data = fixture });
- var path_buf: [256]u8 = undefined;
- const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/search.pdf", .{tmp.sub_path});
- const p = try Pardes.init(gpa, .{ .file = path, .cols = 80, .rows = 28 });
- defer p.deinit();
- p.native_images = true;
- const pane = p.panes[0].?;
- const state = &pane.pdf.?;
- try state.setSearchQuery(p.pdf_gpa, "Pardes");
- var frame = std.heap.ArenaAllocator.init(gpa);
- defer frame.deinit();
- _ = try p.render(frame.allocator());
- try std.testing.expect(state.search_results != null);
- try std.testing.expect(state.search_results.?.hit_count > 0);
- try std.testing.expect(state.reveal_viewport_valid);
-
- // Reading moved on after the original reveal. A reload retains the query
- // and rebuilds its quads, but it must not mistake that for a new request to
- // jump back to the hit.
- const anchor_page = state.page;
- const anchor_fraction: f64 = 0.82;
- state.document_scroll_y = @as(f64, @floatFromInt(state.page_starts[anchor_page])) +
- anchor_fraction * @as(f64, @floatFromInt(state.page_heights[anchor_page]));
- state.scroll_to_page_pending = false;
- state.layout_anchor_pending = false;
- state.search_reveal_pending = false;
- const reveal_w = state.reveal_viewport_w;
- const reveal_h = state.reveal_viewport_h;
- const reveal_fit = state.reveal_fit;
-
- // Same semantic document on a fresh generation keeps the expected anchor
- // easy to state while still rebuilding every MuPDF-owned search object.
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "search.pdf", .data = fixture });
- try std.testing.expect(p.reloadWatchedFile(0, &.{}));
- try std.testing.expectEqual(reveal_w, state.reveal_viewport_w);
- try std.testing.expectEqual(reveal_h, state.reveal_viewport_h);
- try std.testing.expectEqual(reveal_fit, state.reveal_fit);
- try std.testing.expect(!state.search_reveal_pending);
- _ = frame.reset(.retain_capacity);
- _ = try p.render(frame.allocator());
-
- const page = @min(anchor_page, state.page_count - 1);
- const anchored = @as(f64, @floatFromInt(state.page_starts[page])) +
- anchor_fraction * @as(f64, @floatFromInt(state.page_heights[page]));
- const viewport = pdf_pane.paneViewport(p, pane).?;
- const max_scroll = @as(f64, @floatFromInt(state.document_height -| viewport.pixel_h));
- try std.testing.expectApproxEqAbs(@min(anchored, max_scroll), state.document_scroll_y, 0.001);
-}
-
-test "MuPDF pane renders, navigates, searches, and round-trips its page" {
- if (!pdf_enabled or platform == .web) return;
-
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{
- .file = "docs/design.pdf",
- .cols = 80,
- .rows = 24,
- });
- defer p.deinit();
- const pane = p.panes[0].?;
- try std.testing.expect(hasPdf(pane));
- try std.testing.expect(pane.pdf.?.page_count > 1);
- try std.testing.expectEqual(PdfFitMode.width, pane.pdf.?.fit);
- try std.testing.expectEqual(@as(u16, 0), pane.pdf.?.pan_x);
- try std.testing.expectEqual(@as(u16, 0), pane.pdf.?.pan_y);
- try std.testing.expectEqual(@as(u16, 8), p.cell_pixels.w);
- try std.testing.expectEqual(@as(u16, 16), p.cell_pixels.h);
- const initial_tag = try pardes.pdf_test.tagText(p, p.scratch.allocator(), pane);
- try std.testing.expect(std.mem.indexOf(u8, initial_tag, "pdf 1/") != null);
- try std.testing.expect(std.mem.indexOf(u8, initial_tag, " width ") != null);
- try std.testing.expect(std.mem.indexOf(u8, initial_tag, " height ") == null);
- try std.testing.expect(std.mem.indexOf(u8, initial_tag, "PdfFit") != null);
- try std.testing.expect(std.mem.endsWith(u8, initial_tag, pane_tail));
-
- p.native_images = true;
- var frame = std.heap.ArenaAllocator.init(gpa);
- defer frame.deinit();
- const first = try p.render(frame.allocator());
- try std.testing.expectEqual(@as(usize, 1), first.nimages);
- const first_place = first.images[0].?;
- try std.testing.expect(first_place.rgba.len == first_place.iw * first_place.ih * 4);
- try std.testing.expectEqual(image.NativeFit.width, first_place.native.fit);
- const request = pdf_pane.renderRequest(
- pdf_pane.paneViewport(p, pane) orelse return error.MissingPdfViewport,
- pdf_raster_policy,
- );
- try std.testing.expectEqual(pdf_raster_policy.dpi, request.dpi);
- try std.testing.expectEqual(pdf_raster_policy.max_dimension, request.max_dimension);
- try std.testing.expectEqual(
- request,
- pdf_pane.rasterForPage(&pane.pdf.?, pane.pdf.?.page).?.request,
- );
- try std.testing.expect(@max(first_place.iw, first_place.ih) <= request.max_dimension);
- if (pdf_raster_policy.match_viewport) {
- const viewport = pdf_pane.paneViewport(p, pane) orelse return error.MissingPdfViewport;
- try std.testing.expectEqual(viewport.pixel_w, request.minimum_width);
- try std.testing.expectEqual(viewport.pixel_h, request.minimum_height);
- try std.testing.expect(first_place.iw >= viewport.pixel_w or
- @max(first_place.iw, first_place.ih) == request.max_dimension);
- try std.testing.expect(first_place.ih >= viewport.pixel_h or
- @max(first_place.iw, first_place.ih) == request.max_dimension);
- } else {
- try std.testing.expectEqual(@as(u32, 0), request.minimum_width);
- try std.testing.expectEqual(@as(u32, 0), request.minimum_height);
- }
-
- // SDL's raw dy path retains fractions in the placement itself; it does
- // not leave native pixels fixed while only the underlying cells slide.
- p.update(.{ .pdf_scroll = .{ .pane = 0, .delta_pixels = 0.25 } });
- try std.testing.expectEqual(@as(f64, 0.25), pane.pdf.?.document_scroll_y);
- _ = frame.reset(.retain_capacity);
- const fractional = try p.render(frame.allocator());
- try std.testing.expectEqual(first_place.native.revision, fractional.images[0].?.native.revision);
- try std.testing.expectEqual(@as(f32, -0.25), fractional.images[0].?.native.pixel_offset_y);
- pane.pdf.?.document_scroll_y = 0;
-
- // The default reading view moves one exact display-cell distance without
- // replacing page pixels. Document placement, not texture identity, moves.
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(usize, 0), pane.pdf.?.page);
- try std.testing.expectEqual(@as(f64, p.cell_pixels.h), pane.pdf.?.document_scroll_y);
- try std.testing.expectEqual(
- first_place.native.revision,
- pdf_pane.rasterForPage(&pane.pdf.?, pane.pdf.?.page).?.revision,
- );
- _ = frame.reset(.retain_capacity);
- const panned = try p.render(frame.allocator());
- try std.testing.expectEqual(first_place.native.revision, panned.images[0].?.native.revision);
- try std.testing.expect(panned.images[0].?.native.geometry.?.src.y > first_place.native.geometry.?.src.y);
-
- const row_scroll = pane.pdf.?.document_scroll_y;
- pane.pdf.?.document_scroll_y = 0;
- p.update(.{ .key = .{ .cp = '3' } });
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(f64, p.cell_pixels.h * 3), pane.pdf.?.document_scroll_y);
-
- // Counts survive a shared multi-key prefix. An invalid continuation is
- // consumed and clears both prefix and count before the following action.
- pane.pdf.?.document_scroll_y = 0;
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(f64, p.cell_pixels.h * 2), pane.pdf.?.document_scroll_y);
- pane.pdf.?.document_scroll_y = 0;
- p.update(.{ .key = .{ .cp = '4' } });
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = '?' } });
- try std.testing.expectEqual(@as(u21, 0), pane.pending);
- try std.testing.expectEqual(@as(u32, 0), pane.count);
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(row_scroll, pane.pdf.?.document_scroll_y);
-
- pane.pdf.?.document_scroll_y = 0;
- p.update(.{ .key = .{ .cp = 'd', .ctrl = true } });
- const half_scroll = pane.pdf.?.document_scroll_y;
- try std.testing.expect(half_scroll > row_scroll);
- pane.pdf.?.document_scroll_y = 0;
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'd', .ctrl = true } });
- const counted_half = pane.pdf.?.document_scroll_y;
- try std.testing.expectEqual(half_scroll * 2, counted_half);
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'u', .ctrl = true } });
- try std.testing.expectEqual(@as(f64, 0), pane.pdf.?.document_scroll_y);
- pane.pdf.?.document_scroll_y = @as(f64, p.cell_pixels.h) * 4;
- p.update(.{ .key = .{ .cp = '3' } });
- p.update(.{ .key = .{ .cp = 'k' } });
- try std.testing.expectEqual(row_scroll, pane.pdf.?.document_scroll_y);
- pane.pdf.?.document_scroll_y = 0;
- p.update(.{ .key = .{ .cp = 'f', .ctrl = true } });
- try std.testing.expect(pane.pdf.?.document_scroll_y >= half_scroll);
- pane.pdf.?.document_scroll_y = 0;
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'f', .ctrl = true } });
- const key_viewport = pdf_pane.paneViewport(p, pane).?;
- const max_key_scroll = @as(f64, @floatFromInt(pane.pdf.?.document_height -| key_viewport.pixel_h));
- const counted_full = @min(@as(f64, @floatFromInt(key_viewport.pixel_h * 2)), max_key_scroll);
- try std.testing.expectEqual(counted_full, pane.pdf.?.document_scroll_y);
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'b', .ctrl = true } });
- try std.testing.expectEqual(@max(@as(f64, 0), counted_full - @as(f64, @floatFromInt(key_viewport.pixel_h * 2))), pane.pdf.?.document_scroll_y);
- try std.testing.expectEqual(
- first_place.native.revision,
- pdf_pane.rasterForPage(&pane.pdf.?, pane.pdf.?.page).?.revision,
- );
-
- // At a page boundary both page rasters coexist, the gap remains uncovered,
- // and a row step crosses it without snapping either page to an edge.
- //
- // This lands by ASSIGNMENT, which is a jump and not a fling — and the
- // travel counter has to say so: the keys above scrolled two screenfuls
- // without any frame in between to spend that distance, which no shell does
- // (every wheel batch is followed by a draw). Left unspent it would make the
- // frame below the first frame of a fling and hand it bands.
- const viewport = pdf_pane.paneViewport(p, pane).?;
- pane.pdf.?.scroll_travel = 0;
- pane.pdf.?.document_scroll_y = @floatFromInt(
- pane.pdf.?.page_starts[1] -| viewport.pixel_h / 2,
- );
- _ = frame.reset(.retain_capacity);
- const boundary = try p.render(frame.allocator());
- try std.testing.expectEqual(@as(usize, 2), boundary.nimages);
- try std.testing.expectEqual(@as(u32, 0), boundary.images[0].?.native.page);
- try std.testing.expectEqual(@as(u32, 1), boundary.images[1].?.native.page);
- const first_bottom = @as(f32, @floatFromInt(
- boundary.images[0].?.native.geometry.?.dst.y +
- boundary.images[0].?.native.geometry.?.dst.h,
- )) + boundary.images[0].?.native.pixel_offset_y;
- const second_top = @as(f32, @floatFromInt(
- boundary.images[1].?.native.geometry.?.dst.y,
- )) + boundary.images[1].?.native.pixel_offset_y;
- const visible_gap = second_top - first_bottom;
- try std.testing.expect(visible_gap >= @as(f32, PDF_PAGE_GAP_PX));
- try std.testing.expect(visible_gap <= @as(f32, PDF_PAGE_GAP_PX + 1));
- const before_boundary_scroll = pane.pdf.?.document_scroll_y;
- const page0_revision = boundary.images[0].?.native.revision;
- const page1_revision = boundary.images[1].?.native.revision;
-
- // A scroll can activate the already-cached neighbor before the shell's
- // next draw. Every consumer resolves the active page's resident raster,
- // so a queued click cannot accidentally use page zero's dimensions.
- pane.pdf.?.document_scroll_y = @as(f64, @floatFromInt(pane.pdf.?.page_starts[1])) - 0.5;
- try std.testing.expectEqual(
- page0_revision,
- pdf_pane.rasterForPage(&pane.pdf.?, 0).?.revision,
- );
- try std.testing.expect(pdf_pane.scrollPane(p, pane, 1));
- try std.testing.expectEqual(@as(usize, 1), pane.pdf.?.page);
- try std.testing.expectEqual(
- page1_revision,
- pdf_pane.rasterForPage(&pane.pdf.?, pane.pdf.?.page).?.revision,
- );
- pdf_pane.activatePage(p, pane, 0, false);
- pane.pdf.?.document_scroll_y = before_boundary_scroll;
-
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(before_boundary_scroll + p.cell_pixels.h, pane.pdf.?.document_scroll_y);
- _ = frame.reset(.retain_capacity);
- const second = try p.render(frame.allocator());
- try std.testing.expectEqual(@as(usize, 2), second.nimages);
- try std.testing.expectEqual(page0_revision, second.images[0].?.native.revision);
- try std.testing.expectEqual(page1_revision, second.images[1].?.native.revision);
- p.update(.{ .key = .{ .cp = 'k' } });
- try std.testing.expectEqual(before_boundary_scroll, pane.pdf.?.document_scroll_y);
-
- // Once page zero is wholly outside the viewport, both its owned RGBA and
- // backend placement disappear; returning later renders a new raster. What
- // does NOT go back is the memory: the departing page's bytes are parked in
- // the relay and the arriving page of the same size takes them, so a fling
- // never asks the allocator (or the kernel's fault handler) for megabytes it
- // just gave up.
- pdf_pane.setPage(p, pane, 1);
- _ = frame.reset(.retain_capacity);
- const away = try p.render(frame.allocator());
- try std.testing.expect(away.nimages > 0);
- for (away.images[0..away.nimages]) |maybe| if (maybe) |place|
- try std.testing.expect(place.native.page != 0);
- try std.testing.expect(pdf_pane.rasterForPage(&pane.pdf.?, 0) == null);
- try std.testing.expectEqual(@as(usize, 1), pane.pdf.?.spare_len);
- const retired = pane.pdf.?.spare[0];
- pdf_pane.setPage(p, pane, 0);
- _ = frame.reset(.retain_capacity);
- const returned = try p.render(frame.allocator());
- try std.testing.expectEqual(@as(u32, 0), returned.images[0].?.native.page);
- try std.testing.expectEqual(retired.ptr, returned.images[0].?.rgba.ptr);
- try std.testing.expect(returned.images[0].?.native.revision != page0_revision);
-
- // PdfFit exists as a real builtin in this build. It resets placement but
- // preserves the current page pixels; fit-height j/k remains continuous in
- // the same document-pixel coordinate space.
- const fit_builtin = std.meta.stringToEnum(Builtin, "PdfFit") orelse
- return error.MissingPdfFitBuiltin;
- const revision_before_toggle = pdf_pane.rasterForPage(
- &pane.pdf.?,
- pane.pdf.?.page,
- ).?.revision;
- try std.testing.expect(pardes.pdf_test.runBuiltin(
- p,
- @tagName(fit_builtin),
- 0,
- "",
- null,
- ));
- try std.testing.expectEqual(PdfFitMode.height, pane.pdf.?.fit);
- try std.testing.expectEqual(@as(u16, 0), pane.pdf.?.pan_x);
- try std.testing.expectEqual(@as(u16, 0), pane.pdf.?.pan_y);
- try std.testing.expectEqual(
- revision_before_toggle,
- pdf_pane.rasterForPage(&pane.pdf.?, pane.pdf.?.page).?.revision,
- );
- const height_tag = try pardes.pdf_test.tagText(p, p.scratch.allocator(), pane);
- try std.testing.expect(std.mem.indexOf(u8, height_tag, "pdf 1/") != null);
- try std.testing.expect(std.mem.indexOf(u8, height_tag, " width ") == null);
- try std.testing.expect(std.mem.indexOf(u8, height_tag, " height ") != null);
- try std.testing.expect(std.mem.indexOf(u8, height_tag, "PdfFit") != null);
- for (p.panes) |slot| {
- const other = slot orelse continue;
- if (hasPdf(other)) continue;
- pdf_pane.toggleFit(other); // pane-scoped and deliberately inert here
- try std.testing.expectEqual(PdfFitMode.height, pane.pdf.?.fit);
- break;
- }
-
- // A fit-height landscape page exposes horizontal overflow to a horizontal
- // wheel without rerasterizing. Use synthetic dimensions only for the
- // geometry check; no frame is drawn while they differ from the buffer.
- const active_raster = pdf_pane.rasterForPage(&pane.pdf.?, pane.pdf.?.page).?;
- const saved_iw = active_raster.iw;
- const saved_ih = active_raster.ih;
- active_raster.iw = 2000;
- active_raster.ih = 500;
- pdf_pane.horizontalWheel(p, pane, 1);
- try std.testing.expect(pane.pdf.?.pan_x > 0);
- try std.testing.expectEqual(revision_before_toggle, active_raster.revision);
- pane.pdf.?.pan_x = 0;
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'l' } });
- try std.testing.expect(pane.pdf.?.pan_x > 0);
- p.update(.{ .key = .{ .cp = '$' } });
- try std.testing.expectEqual(std.math.maxInt(u16), pane.pdf.?.pan_x);
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'h' } });
- try std.testing.expect(pane.pdf.?.pan_x < std.math.maxInt(u16));
- p.update(.{ .key = .{ .cp = '0' } });
- try std.testing.expectEqual(@as(u16, 0), pane.pdf.?.pan_x);
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'l' } });
- try std.testing.expectEqual(std.math.maxInt(u16), pane.pdf.?.pan_x);
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'h' } });
- try std.testing.expectEqual(@as(u16, 0), pane.pdf.?.pan_x);
- active_raster.iw = saved_iw;
- active_raster.ih = saved_ih;
- pane.pdf.?.pan_x = 0;
-
- const before_height_scroll = pane.pdf.?.document_scroll_y;
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expect(pane.pdf.?.document_scroll_y > before_height_scroll);
-
- const revision_before_search = active_raster.revision;
- try p.runSearch(0, "Pardes", .text, .top);
- try std.testing.expectEqualStrings("Pardes", pane.pdf.?.search_query);
- try std.testing.expectEqual(revision_before_search, active_raster.revision);
- const results_id = pane.search_pane orelse return error.MissingPdfSearchResults;
- const results = p.panes[results_id].?.file.?.content;
- try std.testing.expect(std.mem.indexOf(u8, results, "design.pdf:") != null);
- // n SELECTS the first result row and opens nothing: the walk's only list
- // here is the unlooked +Search buffer, so focus lands THERE. Enter is what
- // jumps, and the document query survives both.
- p.update(.{ .key = .{ .cp = 'n' } });
- try std.testing.expectEqual(results_id, p.active);
- const results_pane = p.panes[results_id].?;
- try std.testing.expect(results_pane.vsel.active and results_pane.vsel.explicit);
- try std.testing.expectEqual(@as(i32, 0), results_pane.cur_row);
- try std.testing.expectEqual(@as(i32, 0), results_pane.cur_col);
- p.update(.{ .key = .{ .cp = Key.enter } });
- try std.testing.expectEqual(@as(usize, 0), p.active);
- try std.testing.expectEqualStrings("Pardes", pane.pdf.?.search_query);
-
- // Search state is owned and untruncated, and changing pages invalidates
- // only page-local state while retaining the document query.
- const long_query = "a query deliberately longer than any tag display budget: " ++
- "012345678901234567890123456789012345678901234567890123456789" ++
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
- try pane.pdf.?.setSearchQuery(p.pdf_gpa, long_query);
- try std.testing.expectEqualStrings(long_query, pane.pdf.?.search_query);
- const next_page = if (pane.pdf.?.page == 0) @as(usize, 1) else 0;
- pdf_pane.setPage(p, pane, next_page);
- try std.testing.expectEqualStrings(long_query, pane.pdf.?.search_query);
-
- // PDF dumps intentionally retain the existing image-compatible schema:
- // page/path and the exact editable tail are restored, while pane-local
- // tint starts from the fresh-PDF default rather than being serialized.
- pdf_pane.toggleTint(pane);
- try std.testing.expectEqual(PdfTintMode.full, pane.pdf.?.tint);
- pane.pdf.?.fit = .height;
- const custom_tail = " Keep Del";
- @memcpy(pane.tag_tail[0..custom_tail.len], custom_tail);
- pane.tag_tail_len = custom_tail.len;
- pane.tag_init = true;
- try p.dumpState();
- const restored = try Pardes.initFromDump(gpa, .{}, p.dump_out.?);
- defer restored.deinit();
- try std.testing.expect(hasPdf(restored.panes[0].?));
- try std.testing.expectEqual(pane.pdf.?.page, restored.panes[0].?.pdf.?.page);
- try std.testing.expectEqual(PdfFitMode.width, restored.panes[0].?.pdf.?.fit);
- try std.testing.expectEqual(PdfTintMode.filtered, restored.panes[0].?.pdf.?.tint);
- const restored_pane = restored.panes[0].?;
- try std.testing.expect(restored_pane.tag_init);
- try std.testing.expectEqualStrings(
- custom_tail,
- restored_pane.tag_tail[0..restored_pane.tag_tail_len],
- );
-}
-
-test "a fling's banded pages show the reader exactly what whole pages would" {
- if (!pdf_enabled or platform == .web) return;
-
- // The contract fast scrolling is allowed to change: HOW pixels are carried
- // (a strip of a page instead of the page) but never WHICH pixels arrive. So
- // the same frame is drawn twice — once at fling speed, once at reading
- // speed — and every pixel inside every source rectangle must match, along
- // with where on screen it goes.
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{ .file = "docs/design.pdf", .cols = 120, .rows = 40 });
- defer p.deinit();
- p.native_images = true;
- const pane = p.panes[0].?;
- const pv = &pane.pdf.?;
- var frame = std.heap.ArenaAllocator.init(gpa);
- defer frame.deinit();
- _ = try p.render(frame.allocator());
-
- const viewport = pdf_pane.paneViewport(p, pane) orelse return error.MissingPdfViewport;
- // Land mid-page-boundary so the frame carries TWO pages, each showing a
- // fraction of itself — the shape a fling actually produces.
- const landing = @as(f64, @floatFromInt(pv.page_starts[1] -| viewport.pixel_h / 3));
- const Shot = struct {
- page: u32,
- dst: image.PixelRect,
- pixels: []u8,
- };
- var shots: [8]Shot = undefined;
- var shots_len: usize = 0;
- defer for (shots[0..shots_len]) |shot| gpa.free(shot.pixels);
-
- // A fling: one frame's worth of wheel travel carrying the viewport more
- // than a screenful, delivered through the real scroll path so the distance
- // is counted the way a wheel batch counts it.
- pv.document_scroll_y = 0;
- pv.scroll_travel = 0;
- try std.testing.expect(pdf_pane.scrollPane(p, pane, landing));
- _ = frame.reset(.retain_capacity);
- const flung = try p.render(frame.allocator());
- try std.testing.expect(flung.nimages >= 2);
- var banded = false;
- for (flung.images[0..flung.nimages]) |maybe| {
- const place = maybe orelse continue;
- const geometry = place.native.geometry orelse return error.MissingPdfGeometry;
- const raster = pdf_pane.rasterForPage(pv, place.native.page) orelse
- return error.MissingPdfRaster;
- if (raster.band_h < raster.ih) banded = true;
- try std.testing.expectEqual(place.iw * raster.band_h * 4, place.rgba.len);
- shots[shots_len] = .{
- .page = place.native.page,
- .dst = geometry.dst,
- .pixels = try copySourceRect(gpa, place, geometry.src),
- };
- shots_len += 1;
- }
- // ...and it really did band, or the comparison below is two identical
- // whole-page renders agreeing with each other.
- try std.testing.expect(banded);
- // ...at reading speed: no travel at all since the frame above, so every
- // page is rasterized whole again, and that is the picture the banded frame
- // has to have matched.
- _ = frame.reset(.retain_capacity);
- const rested = try p.render(frame.allocator());
- try std.testing.expectEqual(shots_len, rested.nimages);
- for (rested.images[0..rested.nimages], shots[0..shots_len]) |maybe, shot| {
- const place = maybe orelse return error.MissingPdfPlacement;
- const geometry = place.native.geometry orelse return error.MissingPdfGeometry;
- const raster = pdf_pane.rasterForPage(pv, place.native.page) orelse
- return error.MissingPdfRaster;
- try std.testing.expectEqual(raster.ih, raster.band_h); // promoted at rest
- try std.testing.expectEqual(shot.page, place.native.page);
- try std.testing.expectEqual(shot.dst.x, geometry.dst.x);
- try std.testing.expectEqual(shot.dst.y, geometry.dst.y);
- try std.testing.expectEqual(shot.dst.w, geometry.dst.w);
- try std.testing.expectEqual(shot.dst.h, geometry.dst.h);
- const whole = try copySourceRect(gpa, place, geometry.src);
- defer gpa.free(whole);
- try std.testing.expectEqualSlices(u8, shot.pixels, whole);
- }
-}
-
-/// The pixels a backend samples out of one placement: the source rectangle,
-/// row by row, at the texture's own stride. Test-only, and the one operation
-/// that makes "same picture" mean something when the textures differ in shape.
-fn copySourceRect(
- gpa: std.mem.Allocator,
- place: ImagePlace,
- src: image.PixelRect,
-) ![]u8 {
- const stride = place.iw * 4;
- const row_len = @as(usize, src.w) * 4;
- const out = try gpa.alloc(u8, row_len * src.h);
- errdefer gpa.free(out);
- var row: usize = 0;
- while (row < src.h) : (row += 1) {
- const from = (@as(usize, src.y) + row) * stride + @as(usize, src.x) * 4;
- @memcpy(out[row * row_len ..][0..row_len], place.rgba[from..][0..row_len]);
- }
- return out;
-}
-
-test "PDF normal adapter consumes unsupported actions and navigates page fallback" {
- if (!pdf_enabled or platform == .web) return;
-
- const p = try Pardes.init(std.testing.allocator, .{
- .file = "docs/design.pdf",
- .cols = 80,
- .rows = 24,
- });
- defer p.deinit();
- const pane = p.panes[0].?;
- const pv = &pane.pdf.?;
- try std.testing.expect(pv.page_count > 3);
- p.native_images = false;
-
- // Every vertical vocabulary falls back to counted page changes when the
- // shell cannot place native pixels.
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(usize, 2), pv.page);
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'k' } });
- try std.testing.expectEqual(@as(usize, 0), pv.page);
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'd', .ctrl = true } });
- try std.testing.expectEqual(@as(usize, 2), pv.page);
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'u', .ctrl = true } });
- try std.testing.expectEqual(@as(usize, 0), pv.page);
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'f', .ctrl = true } });
- try std.testing.expectEqual(@as(usize, 2), pv.page);
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'b', .ctrl = true } });
- try std.testing.expectEqual(@as(usize, 0), pv.page);
-
- // Prefix actions and the counted text goto-line action map to pages.
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'e' } });
- try std.testing.expectEqual(pv.page_count - 1, pv.page);
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'g' } });
- try std.testing.expectEqual(@as(usize, 0), pv.page);
- p.update(.{ .key = .{ .cp = '3' } });
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'g' } });
- try std.testing.expectEqual(@as(usize, 2), pv.page);
- p.update(.{ .key = .{ .cp = '2' } });
- p.update(.{ .key = .{ .cp = 'G' } });
- try std.testing.expectEqual(@as(usize, 1), pv.page);
-
- // Editing/selection actions are consumed no-ops: parser state clears,
- // placeholder cells never acquire a range, and PDF state stays intact.
- const page_before_noop = pv.page;
- const revision_before_noop = pv.next_raster_revision;
- p.update(.{ .key = .{ .cp = '4' } });
- p.update(.{ .key = .{ .cp = 'd' } });
- p.update(.{ .key = .{ .cp = 'v' } });
- p.update(.{ .key = .{ .cp = '|' } });
- try std.testing.expectEqual(page_before_noop, pv.page);
- try std.testing.expectEqual(revision_before_noop, pv.next_raster_revision);
- try std.testing.expectEqual(@as(u32, 0), pane.count);
- try std.testing.expectEqual(@as(u21, 0), pane.pending);
- try std.testing.expect(!pane.vsel.active and !pane.msel.active and pane.nsel == 0);
- try std.testing.expect(!pane.tag_edit);
-
- // Cross-pane BODY-NORMAL actions keep their established shared paths.
- p.update(.{ .key = .{ .cp = ':' } });
- try std.testing.expect(pane.tag_edit);
- try std.testing.expectEqual(Mode.normal, pane.mode);
- p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expect(!pane.tag_edit);
- p.update(.{ .key = .{ .cp = ' ' } });
- try std.testing.expect(p.leader_on);
- p.update(.{ .key = .{ .cp = '!' } });
- try std.testing.expect(!p.leader_on);
- p.update(.{ .key = .{ .cp = '/' } });
- try std.testing.expect(pane.tag_edit);
- p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expect(!pane.tag_edit);
-}
-
-test "Escape cancels PDF chrome in place and Shift-Escape leaves the pane" {
- if (!pdf_enabled or platform == .web) return;
-
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{
- .file = "docs/design.pdf",
- .cols = 80,
- .rows = 28,
- });
- defer p.deinit();
- const pane = p.panes[0].?;
- const pv = &pane.pdf.?;
- p.native_images = true;
-
- // A second pane, then focus it and come back: `Last` has somewhere to go
- // and the PDF is where the keys land.
- pdf_pane.openSections(p, 0);
- p.update(.{ .key = .{ .cp = 'w', .ctrl = true } });
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(usize, 1), p.active);
- p.update(.{ .key = .{ .cp = 'w', .ctrl = true } });
- p.update(.{ .key = .{ .cp = 'k' } });
- try std.testing.expectEqual(@as(usize, 0), p.active);
-
- // Everything a reader can see over the page: a search overlay and a live
- // word selection.
- try pv.setSearchQuery(p.pdf_gpa, "Pardes");
- var found = try pv.document.search(gpa, pv.page, "Pardes");
- defer found.deinit(gpa);
- try std.testing.expect(found.quads.len > 0);
- const quad = found.quads[0].quad;
- try std.testing.expectEqual(
- pdf_pane.SelectionUpdate.changed,
- pv.setSelection(p.pdf_gpa, quad.ul, quad.lr, false),
- );
- p.update(.{ .key = .{ .cp = '3' } });
- const page_before = pv.page;
- const scroll_before = pv.document_scroll_y;
-
- // Escape drops the overlay and the selection, keeps the reading position,
- // and does NOT hand the keyboard to another pane.
- p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expectEqual(@as(usize, 0), p.active);
- try std.testing.expect(pv.selection == null and pv.selection_text.len == 0);
- try std.testing.expectEqual(@as(usize, 0), pv.search_query.len);
- try std.testing.expect(pv.search_results == null);
- try std.testing.expect(!pv.highlights_dirty and !pv.search_reveal_pending);
- try std.testing.expectEqual(page_before, pv.page);
- try std.testing.expectEqual(scroll_before, pv.document_scroll_y);
- try std.testing.expectEqual(@as(u32, 0), pane.count);
- try std.testing.expectEqual(@as(u21, 0), pane.pending);
- // A second Escape on a bare document is inert rather than an exit.
- p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expectEqual(@as(usize, 0), p.active);
-
- // Shift-Escape is the way out, and it leaves the document as it found it.
- p.update(.{ .key = .{ .cp = Key.escape, .shift = true } });
- try std.testing.expectEqual(@as(usize, 1), p.active);
- try std.testing.expectEqual(page_before, pv.page);
- try std.testing.expectEqual(scroll_before, pv.document_scroll_y);
-}
-
-test "PDF continuous strip renders every intersecting short page" {
- if (!pdf_enabled or platform == .web) return;
-
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{
- .file = "docs/design.pdf",
- .cols = 80,
- .rows = 24,
- });
- defer p.deinit();
- const pane = p.panes[0].?;
- const pv = &pane.pdf.?;
- try std.testing.expect(pv.page_count > 3);
- p.native_images = true;
-
- // A legal wide MediaBox can make more than three pages intersect one
- // viewport. Seed tiny matching rasters so this tests transport/cache
- // cardinality without spending the unit test rendering fake page sizes.
- for (pv.page_sizes) |*size| size.* = .{ .width = 100_000, .height = 1 };
- pv.layout_valid = false;
- pv.scroll_to_page_pending = true;
- const viewport = pdf_pane.ensurePaneLayout(p, pane).?;
- const visible = pdf_pane.visiblePages(pv, viewport);
- try std.testing.expectEqual(pv.page_count, visible.len);
- const request = pdf_pane.renderRequest(viewport, pdf_raster_policy);
- try std.testing.expect(pv.page_count <= pv.rasters.len);
- for (0..pv.page_count) |page| {
- const rgba = try gpa.alloc(u8, @as(usize, viewport.pixel_w) * 4);
- @memset(rgba, @intCast(page));
- pv.rasters[pv.rasters_len] = .{
- .valid = true,
- .page = page,
- .rgba = rgba,
- .iw = viewport.pixel_w,
- .ih = 1,
- .request = request,
- .request_valid = true,
- .tried = true,
- .tint_key = .{ .mode = pv.tint, .colors = pdf_pane.tintColors(p) },
- .revision = @intCast(page + 1),
- };
- pv.rasters_len += 1;
- }
-
- var frame = std.heap.ArenaAllocator.init(gpa);
- defer frame.deinit();
- const surface = try p.render(frame.allocator());
- try std.testing.expectEqual(pv.page_count, surface.nimages);
- try std.testing.expectEqual(pv.page_count, pv.rasters_len);
- for (surface.images[0..surface.nimages], 0..) |maybe, page| {
- const place = maybe orelse return error.MissingShortPdfPage;
- try std.testing.expectEqual(@as(u32, @intCast(page)), place.native.page);
- try std.testing.expectEqual(@as(u32, 1), place.native.geometry.?.dst.h);
- }
-}
-
-test "PdfTint cycles pane-local state and exposes it in the live PDF tag" {
- if (!pdf_enabled or platform == .web) return;
-
- const p = try Pardes.init(std.testing.allocator, .{
- .file = "docs/design.pdf",
- .cols = 80,
- .rows = 24,
- });
- defer p.deinit();
- const pane = p.panes[0].?;
- const pv = &pane.pdf.?;
- const tint_builtin = std.meta.stringToEnum(Builtin, "PdfTint") orelse
- return error.MissingPdfTintBuiltin;
-
- try std.testing.expectEqual(PdfTintMode.filtered, pv.tint);
- const initial_tag = try pardes.pdf_test.tagText(p, p.scratch.allocator(), pane);
- try std.testing.expect(std.mem.indexOf(
- u8,
- initial_tag,
- "width PdfFit filtered PdfTint",
- ) != null);
-
- try std.testing.expect(pardes.pdf_test.runBuiltin(
- p,
- @tagName(tint_builtin),
- 0,
- "",
- null,
- ));
- try std.testing.expectEqual(PdfTintMode.full, pv.tint);
- const full_tag = try pardes.pdf_test.tagText(p, p.scratch.allocator(), pane);
- try std.testing.expect(std.mem.indexOf(u8, full_tag, "full PdfTint") != null);
-
- // Running the same pane-scoped word in a terminal cannot mutate the PDF
- // next to it. A FILE boot is one pane now, so that terminal is asked for
- // here rather than inherited from init.
- _ = try p.newShell(1, "");
- _ = p.layoutSplitColumn(0, 1, false);
- pardes.pdf_test.sync(p); // rects for the new column, exactly as the two-pane boot did
- try std.testing.expect(!hasPdf(p.panes[1].?));
- try std.testing.expect(pardes.pdf_test.runBuiltin(
- p,
- @tagName(tint_builtin),
- 1,
- "",
- null,
- ));
- try std.testing.expectEqual(PdfTintMode.full, pv.tint);
-
- try std.testing.expect(pardes.pdf_test.runBuiltin(
- p,
- @tagName(tint_builtin),
- 0,
- "",
- null,
- ));
- try std.testing.expectEqual(PdfTintMode.disabled, pv.tint);
- const disabled_tag = try pardes.pdf_test.tagText(p, p.scratch.allocator(), pane);
- try std.testing.expect(std.mem.indexOf(u8, disabled_tag, "disabled PdfTint") != null);
- try std.testing.expect(pardes.pdf_test.runBuiltin(
- p,
- @tagName(tint_builtin),
- 0,
- "",
- null,
- ));
- try std.testing.expectEqual(PdfTintMode.filtered, pv.tint);
- try std.testing.expectEqual(PdfFitMode.width, pv.fit);
-
- // `dark` intentionally leaves page bg/fg null. PDF tint resolves those
- // deterministically to its chrome colors rather than host defaults.
- const dark_index = for (themes, 0..) |th, i| {
- if (std.mem.eql(u8, th.name, "dark")) break i;
- } else return error.MissingDarkTheme;
- p.setThemeIndex(dark_index);
- const colors = pdf_pane.tintColors(p);
- try std.testing.expectEqual(p.theme().tag_bg, colors.background);
- try std.testing.expectEqual(p.theme().tag_fg, colors.foreground);
-}
-
-test "PDF tint and tinted theme changes rebuild every visible raster only" {
- if (!pdf_enabled or platform == .web) return;
-
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{
- .file = "docs/design.pdf",
- .cols = 80,
- .rows = 24,
- });
- defer p.deinit();
- const pane = p.panes[0].?;
- const pv = &pane.pdf.?;
- p.native_images = true;
- var frame = std.heap.ArenaAllocator.init(gpa);
- defer frame.deinit();
-
- _ = try p.render(frame.allocator());
- const viewport = pdf_pane.paneViewport(p, pane).?;
- pv.document_scroll_y = @floatFromInt(pv.page_starts[1] -| viewport.pixel_h / 2);
- _ = frame.reset(.retain_capacity);
- const default_surface = try p.render(frame.allocator());
- try std.testing.expectEqual(PdfTintMode.filtered, pv.tint);
- try std.testing.expectEqual(@as(usize, 2), default_surface.nimages);
- try std.testing.expectEqual(@as(usize, 2), pv.rasters_len);
-
- const Snapshot = struct {
- page: u32,
- revision: u32,
- geometry: image.NativeGeometry,
- pixel_offset_y: f32,
- iw: usize,
- ih: usize,
- checksum: u64,
- };
- const Capture = struct {
- fn get(surface: *const Surface) ![2]Snapshot {
- if (surface.nimages != 2) return error.UnexpectedVisiblePdfCount;
- var out: [2]Snapshot = undefined;
- for (&out, 0..) |*snapshot, i| {
- const place = surface.images[i] orelse return error.MissingVisiblePdf;
- snapshot.* = .{
- .page = place.native.page,
- .revision = place.native.revision,
- .geometry = place.native.geometry orelse return error.MissingPdfGeometry,
- .pixel_offset_y = place.native.pixel_offset_y,
- .iw = place.iw,
- .ih = place.ih,
- .checksum = std.hash.Wyhash.hash(0x5044_4654_494e_5421, place.rgba),
- };
- }
- return out;
- }
-
- fn expectGeometry(before: [2]Snapshot, after: [2]Snapshot) !void {
- for (before, after) |old, new| {
- try std.testing.expectEqual(old.page, new.page);
- try std.testing.expectEqual(old.geometry, new.geometry);
- try std.testing.expectEqual(old.pixel_offset_y, new.pixel_offset_y);
- try std.testing.expectEqual(old.iw, new.iw);
- try std.testing.expectEqual(old.ih, new.ih);
- }
- }
- };
- const default_filtered = try Capture.get(default_surface);
- for (pv.rasters[0..pv.rasters_len]) |raster|
- try std.testing.expectEqual(PdfTintMode.filtered, raster.tint_key.?.mode);
-
- const dark_index = for (themes, 0..) |th, i| {
- if (std.mem.eql(u8, th.name, "dark")) break i;
- } else return error.MissingDarkTheme;
- p.setThemeIndex(dark_index);
- for (pv.rasters[0..pv.rasters_len]) |raster| try std.testing.expect(!raster.tried);
- _ = frame.reset(.retain_capacity);
- const themed_surface = try p.render(frame.allocator());
- const themed = try Capture.get(themed_surface);
- try Capture.expectGeometry(default_filtered, themed);
- for (default_filtered, themed) |old, new| {
- try std.testing.expect(new.revision > old.revision);
- try std.testing.expect(new.checksum != old.checksum);
- }
-
- // Chrome animation reads a separate palette. Its ticks must never disturb
- // the target-theme tint key or ask MuPDF for the same pixels again.
- try std.testing.expect(p.animationActive());
- for (0..animation.transition_steps) |_| {
- p.update(.tick);
- _ = frame.reset(.retain_capacity);
- const tick_surface = try p.render(frame.allocator());
- const ticked = try Capture.get(tick_surface);
- try Capture.expectGeometry(themed, ticked);
- for (themed, ticked) |once, after_tick| {
- try std.testing.expectEqual(once.revision, after_tick.revision);
- try std.testing.expectEqual(once.checksum, after_tick.checksum);
- }
- }
- try std.testing.expect(!p.animationActive());
-
- pdf_pane.toggleTint(pane);
- try std.testing.expectEqual(PdfTintMode.full, pv.tint);
- _ = frame.reset(.retain_capacity);
- const full_surface = try p.render(frame.allocator());
- const full = try Capture.get(full_surface);
- try Capture.expectGeometry(themed, full);
- for (themed, full) |old, new| {
- try std.testing.expect(new.revision > old.revision);
- }
- for (pv.rasters[0..pv.rasters_len]) |raster|
- try std.testing.expectEqual(PdfTintMode.full, raster.tint_key.?.mode);
-
- pdf_pane.toggleTint(pane);
- try std.testing.expectEqual(PdfTintMode.disabled, pv.tint);
- _ = frame.reset(.retain_capacity);
- const disabled_surface = try p.render(frame.allocator());
- const disabled = try Capture.get(disabled_surface);
- try Capture.expectGeometry(full, disabled);
- for (full, disabled) |old, source| {
- try std.testing.expect(source.revision > old.revision);
- try std.testing.expect(source.checksum != old.checksum);
- }
-
- const acme_index = for (themes, 0..) |th, i| {
- if (std.mem.eql(u8, th.name, "acme")) break i;
- } else return error.MissingAcmeTheme;
- for (pv.rasters[0..pv.rasters_len]) |raster| try std.testing.expect(raster.tried);
- p.setThemeIndex(acme_index);
- // Disabled keys ignore theme colors, so neither explicit invalidation nor
- // the per-raster key comparison asks MuPDF for replacement pixels.
- for (pv.rasters[0..pv.rasters_len]) |raster| try std.testing.expect(raster.tried);
- _ = frame.reset(.retain_capacity);
- const unchanged_surface = try p.render(frame.allocator());
- const unchanged = try Capture.get(unchanged_surface);
- try Capture.expectGeometry(disabled, unchanged);
- for (disabled, unchanged) |old, new| {
- try std.testing.expectEqual(old.revision, new.revision);
- try std.testing.expectEqual(old.checksum, new.checksum);
- }
- try std.testing.expectEqual(@as(usize, 2), pv.rasters_len);
-}
-
-test "PDF resize preserves page-relative document position" {
- if (!pdf_enabled or platform == .web) return;
-
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{
- .file = "docs/design.pdf",
- .cols = 80,
- .rows = 24,
- });
- defer p.deinit();
- const pane = p.panes[0].?;
- const pv = &pane.pdf.?;
- p.native_images = true;
- var frame = std.heap.ArenaAllocator.init(gpa);
- defer frame.deinit();
-
- pdf_pane.setPage(p, pane, 1);
- _ = try p.render(frame.allocator());
- const fraction: f64 = 0.375;
- pv.document_scroll_y = @as(f64, @floatFromInt(pv.page_starts[1])) +
- fraction * @as(f64, @floatFromInt(pv.page_heights[1]));
- p.update(.{ .resize = .{
- .cols = 100,
- .rows = 24,
- .cell_pixels = p.cell_pixels,
- } });
- try std.testing.expect(!pv.layout_valid);
- try std.testing.expect(pv.layout_anchor_pending);
- _ = frame.reset(.retain_capacity);
- _ = try p.render(frame.allocator());
- const resized_fraction = (pv.document_scroll_y -
- @as(f64, @floatFromInt(pv.page_starts[1]))) /
- @as(f64, @floatFromInt(pv.page_heights[1]));
- try std.testing.expectApproxEqAbs(fraction, resized_fraction, 0.000_001);
- try std.testing.expectEqual(@as(usize, 1), pv.page);
-
- const held = pv.document_scroll_y;
- p.update(.{ .resize = .{
- .cols = 100,
- .rows = 24,
- .cell_pixels = p.cell_pixels,
- } });
- try std.testing.expectEqual(held, pv.document_scroll_y);
- try std.testing.expect(pv.layout_valid);
-}
-
-test "PDF pane geometry change preserves its page-relative position" {
- if (!pdf_enabled or platform == .web) return;
-
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{
- .file = "docs/design.pdf",
- .cols = 80,
- .rows = 24,
- });
- defer p.deinit();
- // A FILE boot is one pane now and a lone column always fills the window,
- // so the divider drag below needs a second column to take the width from.
- _ = try p.newShell(1, "");
- _ = p.layoutSplitColumn(0, 1, false);
- pardes.pdf_test.sync(p); // rects for the new column, exactly as the two-pane boot did
- const pane = p.panes[0].?;
- const pv = &pane.pdf.?;
- p.native_images = true;
- var frame = std.heap.ArenaAllocator.init(gpa);
- defer frame.deinit();
-
- pdf_pane.setPage(p, pane, 1);
- _ = try p.render(frame.allocator());
- const fraction: f64 = 0.625;
- pv.document_scroll_y = @as(f64, @floatFromInt(pv.page_starts[1])) +
- fraction * @as(f64, @floatFromInt(pv.page_heights[1]));
- const old_width = pv.layout_viewport_w;
-
- // A divider/split changes rects through computeGeom without emitting a
- // shell resize. pdf_pane.ensurePaneLayout anchors against the old layout
- // before rebuilding it for this wider pane.
- const sibling_weight = p.col_weight[1];
- p.col_weight[0] = sibling_weight * 6;
- p.computeGeom();
- try std.testing.expect(pdf_pane.paneViewport(p, pane).?.pixel_w != old_width);
- try std.testing.expect(pv.layout_valid);
- _ = frame.reset(.retain_capacity);
- _ = try p.render(frame.allocator());
- const changed_fraction = (pv.document_scroll_y -
- @as(f64, @floatFromInt(pv.page_starts[1]))) /
- @as(f64, @floatFromInt(pv.page_heights[1]));
- try std.testing.expectApproxEqAbs(fraction, changed_fraction, 0.000_001);
- try std.testing.expectEqual(@as(usize, 1), pv.page);
-
- // An explicit pending reveal wins over an implicit layout anchor.
- pv.page = 2;
- pv.scroll_to_page_pending = true;
- p.col_weight[0] = sibling_weight * 2;
- p.computeGeom();
- _ = frame.reset(.retain_capacity);
- _ = try p.render(frame.allocator());
- try std.testing.expectEqual(
- @as(f64, @floatFromInt(pv.page_starts[2])),
- pv.document_scroll_y,
- );
-}
-
-test "PDF n/N addresses and reveals distinct MuPDF hits on one page" {
- if (!pdf_enabled or platform == .web) return;
-
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{
- .file = "docs/design.pdf",
- .cols = 120,
- .rows = 24,
- });
- defer p.deinit();
- const pane = p.panes[0].?;
- const pv = &pane.pdf.?;
- p.native_images = true;
-
- var page_hits = try pv.document.search(gpa, 0, "Pardes");
- defer page_hits.deinit(gpa);
- try std.testing.expect(page_hits.hit_count >= 3);
-
- try p.runSearch(0, "Pardes", .text, .top);
- const results_id = pane.search_pane orelse return error.MissingPdfSearchResults;
- const rows = p.panes[results_id].?.file.?.content;
- try std.testing.expect(std.mem.indexOf(u8, rows, "design.pdf:1:1 Pardes\n") != null);
- try std.testing.expect(std.mem.indexOf(u8, rows, "design.pdf:1:2 Pardes\n") != null);
- try std.testing.expect(std.mem.indexOf(u8, rows, "design.pdf:1:3 Pardes\n") != null);
-
- // Exaggerate the cell aspect only to make the three fixture hits occupy
- // distinct fit-width crops. The search/reveal math must use the same
- // reported metrics as placement, whatever a backend reports.
- p.update(.{ .resize = .{
- .cols = p.screen_w,
- .rows = p.screen_h,
- .cell_pixels = .{ .w = 16, .h = 4 },
- } });
-
- var frame = std.heap.ArenaAllocator.init(gpa);
- defer frame.deinit();
- try std.testing.expect(pardes.pdf_test.searchStep(p, 0, 1));
- try std.testing.expectEqual(@as(usize, 0), pv.search_hit);
- const first = try p.render(frame.allocator());
- const revision = first.images[0].?.native.revision;
- const raster = pdf_pane.rasterForPage(pv, pv.page).?;
- const first_scroll = pv.document_scroll_y;
- const first_results = pv.search_results orelse return error.MissingPdfPageSearch;
- const first_quad = for (first_results.quads) |item| {
- if (item.hit == 0) break item.quad;
- } else return error.MissingFirstPdfHit;
- const first_y = pdf_pane.normalizedPixel(
- (first_quad.ul.y + first_quad.ur.y + first_quad.ll.y + first_quad.lr.y) / 4,
- raster.ih,
- );
- const first_geometry = pdf_pane.paneGeometry(p, pane) orelse return error.MissingPdfGeometry;
- try std.testing.expect(first_y >= first_geometry.src.y and
- first_y < first_geometry.src.y + first_geometry.src.h);
-
- try std.testing.expect(pardes.pdf_test.searchStep(p, 0, 1));
- try std.testing.expectEqual(@as(usize, 1), pv.search_hit);
- _ = frame.reset(.retain_capacity);
- const second = try p.render(frame.allocator());
- try std.testing.expectEqual(revision, second.images[0].?.native.revision);
-
- try std.testing.expect(pardes.pdf_test.searchStep(p, 0, 1));
- try std.testing.expectEqual(@as(usize, 2), pv.search_hit);
- _ = frame.reset(.retain_capacity);
- const third = try p.render(frame.allocator());
- try std.testing.expectEqual(revision, third.images[0].?.native.revision);
- try std.testing.expect(pv.document_scroll_y != first_scroll);
- const third_results = pv.search_results orelse return error.MissingPdfPageSearch;
- const third_quad = for (third_results.quads) |item| {
- if (item.hit == 2) break item.quad;
- } else return error.MissingThirdPdfHit;
- const third_y = pdf_pane.normalizedPixel(
- (third_quad.ul.y + third_quad.ur.y + third_quad.ll.y + third_quad.lr.y) / 4,
- raster.ih,
- );
- const third_geometry = pdf_pane.paneGeometry(p, pane) orelse return error.MissingPdfGeometry;
- try std.testing.expect(third_y >= third_geometry.src.y and
- third_y < third_geometry.src.y + third_geometry.src.h);
-}
-
-test "PDF native mouse selection, Look, and highlights share page geometry" {
- if (!pdf_enabled or platform == .web) return;
-
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{
- .file = "docs/design.pdf",
- .cols = 80,
- .rows = 24,
- });
- defer p.deinit();
- const pane = p.panes[0].?;
- var frame = std.heap.ArenaAllocator.init(gpa);
- defer frame.deinit();
-
- // A host without Kitty/native pixels keeps the projected-text contract:
- // j/k can still change pages and a body drag remains a generic selection.
- const fallback = try p.render(frame.allocator());
- try std.testing.expectEqual(@as(usize, 0), fallback.nimages);
- p.update(.{ .key = .{ .cp = 'j' } });
- try std.testing.expectEqual(@as(usize, 1), pane.pdf.?.page);
- p.update(.{ .key = .{ .cp = 'k' } });
- try std.testing.expectEqual(@as(usize, 0), pane.pdf.?.page);
- const fallback_rect = p.rects[0];
- const fallback_col = fallback_rect.x + config.GUTTER + 1;
- const fallback_row = fallback_rect.y + BOX_H + 1;
- p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = fallback_col, .row = fallback_row } });
- p.update(.{ .mouse = .{ .button = config.select_button, .kind = .drag, .col = fallback_col + 4, .row = fallback_row } });
- p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = fallback_col + 4, .row = fallback_row } });
- try std.testing.expectEqual(.done, pane.sel[sel_slot].state);
- try std.testing.expect(pane.pdf.?.selection == null);
- pane.sel[sel_slot].state = .none;
-
- p.native_images = true;
- _ = frame.reset(.retain_capacity);
- const plain = try p.render(frame.allocator());
- const plain_revision = plain.images[0].?.native.revision;
-
- var found = try pane.pdf.?.document.search(gpa, 0, "Pardes");
- defer found.deinit(gpa);
- const quad = found.quads[0].quad;
- const nx = (quad.ul.x + quad.ur.x + quad.ll.x + quad.lr.x) / 4;
- const ny = (quad.ul.y + quad.ur.y + quad.ll.y + quad.lr.y) / 4;
- const pv = &pane.pdf.?;
- const raster = pdf_pane.rasterForPage(pv, pv.page).?;
- const source_x: u32 = @intCast(@min(
- raster.iw - 1,
- @as(usize, @intFromFloat(nx * @as(f32, @floatFromInt(raster.iw)))),
- ));
- const source_y: u32 = @intCast(@min(
- raster.ih - 1,
- @as(usize, @intFromFloat(ny * @as(f32, @floatFromInt(raster.ih)))),
- ));
-
- // Bring the known word into the fit-width crop, then invert the shared
- // source/destination geometry to the nearest body cell.
- var geometry = pdf_pane.paneGeometry(p, pane).?;
- if (source_y < geometry.src.y or source_y >= geometry.src.y + geometry.src.h) {
- const overflow = @as(u32, @intCast(raster.ih)) - geometry.src.h;
- const wanted = @min(overflow, source_y -| geometry.src.h / 2);
- pv.pan_y = if (overflow == 0) 0 else @intCast(
- (@as(u64, wanted) * std.math.maxInt(u16) + overflow / 2) / overflow,
- );
- geometry = pdf_pane.paneGeometry(p, pane).?;
- }
- const pixel_x = geometry.dst.x + @as(u32, @intCast(
- @as(u64, source_x - geometry.src.x) * geometry.dst.w / geometry.src.w,
- ));
- const pixel_y = geometry.dst.y + @as(u32, @intCast(
- @as(u64, source_y - geometry.src.y) * geometry.dst.h / geometry.src.h,
- ));
- const r = p.rects[0];
- const base_col: i32 = @intCast(r.x + config.GUTTER + pixel_x / p.cell_pixels.w);
- const base_row: i32 = @intCast(r.y + BOX_H + pixel_y / p.cell_pixels.h);
-
- var selected_col: ?u16 = null;
- var selected_row: u16 = 0;
- const nearby = [_]i32{ 0, -1, 1, -2, 2 };
- find_word: for (nearby) |dy| for (nearby) |dx| {
- const col: u16 = @intCast(std.math.clamp(
- base_col + dx,
- @as(i32, r.x + config.GUTTER),
- @as(i32, r.x + r.w - 1),
- ));
- const row: u16 = @intCast(std.math.clamp(
- base_row + dy,
- @as(i32, r.y + BOX_H),
- @as(i32, r.y + r.h - 1),
- ));
- if (!pdf_pane.beginSelection(p, pane, col, row)) continue;
- if (std.ascii.indexOfIgnoreCase(pv.selection_text, "Pardes") != null) {
- selected_col = col;
- selected_row = row;
- break :find_word;
- }
- };
- const word_col = selected_col orelse return error.PdfMouseMappingMissedWord;
- try std.testing.expect(std.ascii.indexOfIgnoreCase(
- pardes.pdf_test.heldSelection(p, 0).?,
- "Pardes",
- ) != null);
-
- _ = frame.reset(.retain_capacity);
- const selected_frame = try p.render(frame.allocator());
- const selected_revision = selected_frame.images[0].?.native.revision;
- try std.testing.expect(selected_revision != plain_revision);
-
- // Repeating an identical drag endpoint is a no-op: Kitty/SDL keep the
- // same texture generation instead of retransmitting identical pixels.
- try std.testing.expect(pdf_pane.beginSelection(p, pane, word_col, selected_row));
- try std.testing.expect(raster.tried);
- _ = frame.reset(.retain_capacity);
- const duplicate = try p.render(frame.allocator());
- try std.testing.expectEqual(selected_revision, duplicate.images[0].?.native.revision);
-
- // Delayed native hover retains independent quads/text and changes only
- // presentation state. In particular it must not borrow the click path,
- // whose page activation and persistent selection are intentional effects
- // of a gesture rather than observation.
- if (config.look_preview_delay_frames) |delay| {
- const active_before = p.active;
- const page_before = pv.page;
- const scroll_before = pv.document_scroll_y;
- const sels_before = pane.sel;
- const cursor_before = .{ pane.cur_row, pane.cur_col };
- const modal_before = .{ pane.msel, pane.vsel, pane.nsel };
- const selection_before = pv.selection orelse return error.MissingPdfSelection;
- const selection_quads_before = try gpa.dupe(pdf_impl.Quad, selection_before.quads);
- defer gpa.free(selection_quads_before);
- const selection_text_before = try gpa.dupe(u8, pv.selection_text);
- defer gpa.free(selection_text_before);
- const selection_anchor_before = pv.selection_anchor;
- const selection_head_before = pv.selection_head;
- const query_before = try gpa.dupe(u8, pv.search_query);
- defer gpa.free(query_before);
- const search_hit_before = pv.search_hit;
- const jumps_before = .{ p.njumps, p.jcur, p.n_look_src };
- const effects_before = p.effects_len;
- try std.testing.expect(p.drag == .none);
-
- const hover_motion = Event{ .mouse = .{
- .button = .none,
- .kind = .motion,
- .col = word_col,
- .row = selected_row,
- } };
- p.update(hover_motion);
- try std.testing.expect(p.look_hover_wait != null);
- for (0..delay) |_| p.update(.tick);
- const hover = p.pdf_hover_preview orelse return error.MissingPdfHoverPreview;
- try std.testing.expectEqual(@as(usize, 0), hover.probe.page);
- try std.testing.expect(hover.probe.quads.len > 0);
- try std.testing.expect(std.ascii.indexOfIgnoreCase(hover.probe.text, "Pardes") != null);
- try std.testing.expect(p.look_hover_preview == null);
- try std.testing.expect(p.look_hover_wait == null);
-
- const decorated = try pdf_pane.buildHighlights(
- pv,
- p.scratch.allocator(),
- pdf_pane.highlightInput(p, 0, pane),
- );
- const page_highlights = decorated.forPage(pv.page, pv.page);
- try std.testing.expectEqual(pdf_impl.HighlightKind.custom, page_highlights[0].kind);
- try std.testing.expectEqual(pdf_impl.HighlightKind.selection, page_highlights[page_highlights.len - 1].kind);
- _ = frame.reset(.retain_capacity);
- const hovered_frame = try p.render(frame.allocator());
- const hovered_revision = hovered_frame.images[0].?.native.revision;
- try std.testing.expect(hovered_revision != selected_revision);
-
- p.update(.pointer_leave);
- try std.testing.expect(p.pdf_hover_preview == null);
- _ = frame.reset(.retain_capacity);
- const unhovered_frame = try p.render(frame.allocator());
- try std.testing.expect(unhovered_frame.images[0].?.native.revision != hovered_revision);
-
- try std.testing.expectEqual(active_before, p.active);
- try std.testing.expectEqual(page_before, pv.page);
- try std.testing.expectEqual(scroll_before, pv.document_scroll_y);
- try std.testing.expect(std.meta.eql(sels_before, pane.sel));
- try std.testing.expectEqual(cursor_before, .{ pane.cur_row, pane.cur_col });
- try std.testing.expect(std.meta.eql(modal_before, .{ pane.msel, pane.vsel, pane.nsel }));
- const selection_after = pv.selection orelse return error.HoverDroppedPdfSelection;
- try std.testing.expectEqualSlices(pdf_impl.Quad, selection_quads_before, selection_after.quads);
- try std.testing.expectEqualStrings(selection_text_before, pv.selection_text);
- try std.testing.expect(std.meta.eql(selection_anchor_before, pv.selection_anchor));
- try std.testing.expect(std.meta.eql(selection_head_before, pv.selection_head));
- try std.testing.expectEqualStrings(query_before, pv.search_query);
- try std.testing.expectEqual(search_hit_before, pv.search_hit);
- try std.testing.expectEqual(jumps_before, .{ p.njumps, p.jcur, p.n_look_src });
- try std.testing.expectEqual(effects_before, p.effects_len);
- try std.testing.expect(p.drag == .none);
- }
-
- // A native right-click resolves the same MuPDF-snapped word and feeds it
- // to Look. Search highlights precede selection highlights so the live
- // selection remains visually authoritative where they overlap.
- p.update(.{ .mouse = .{ .button = config.look_button, .kind = .press, .col = word_col, .row = selected_row } });
- p.update(.{ .mouse = .{ .button = config.look_button, .kind = .release, .col = word_col, .row = selected_row } });
- try std.testing.expect(std.ascii.indexOfIgnoreCase(pv.search_query, "Pardes") != null);
- pv.resolveSearch(p.pdf_gpa);
- const highlights = (try pdf_pane.buildHighlights(
- pv,
- p.scratch.allocator(),
- pdf_pane.highlightInput(p, 0, pane),
- )).items;
- try std.testing.expect(highlights.len > 1);
- try std.testing.expectEqual(pdf_impl.HighlightKind.search, highlights[0].kind);
- try std.testing.expectEqual(pdf_impl.HighlightKind.selection, highlights[highlights.len - 1].kind);
- const revision_before_reveal = raster.revision;
- const max_document_scroll = @as(f64, @floatFromInt(
- pv.document_height -| pdf_pane.paneViewport(p, pane).?.pixel_h,
- ));
- pv.document_scroll_y = max_document_scroll;
- pdf_pane.revealSearch(pv, pdf_pane.paneViewport(p, pane).?, pdf_pane.paneGeometry(p, pane));
- const revealed_scroll = pv.document_scroll_y;
- try std.testing.expect(revealed_scroll != max_document_scroll);
- try std.testing.expectEqual(revision_before_reveal, raster.revision);
- pv.document_scroll_y = max_document_scroll;
- pdf_pane.revealSearch(
- pv,
- pdf_pane.paneViewport(p, pane).?,
- pdf_pane.paneGeometry(p, pane),
- ); // one shot: subsequent manual pan stays put
- try std.testing.expectEqual(max_document_scroll, pv.document_scroll_y);
- pv.document_scroll_y = revealed_scroll;
- _ = frame.reset(.retain_capacity);
- const searched = try p.render(frame.allocator());
- try std.testing.expect(searched.images[0].?.native.revision != selected_revision);
-
- // Fit and viewport changes can crop a hit which was already revealed.
- // Fit remains placement-only. SDL's physical-viewport quality request
- // changes with a resize and therefore replaces pixels; Kitty's fixed,
- // bandwidth-conscious request retains them.
- const revision_before_geometry_change = searched.images[0].?.native.revision;
- pv.search_reveal_pending = false;
- pdf_pane.toggleFit(pane);
- try std.testing.expect(pv.search_reveal_pending);
- pv.search_reveal_pending = false;
- pdf_pane.toggleFit(pane); // restore the reading-width geometry
- try std.testing.expect(pv.search_reveal_pending);
- pv.search_reveal_pending = false;
- const original_cell_pixels = p.cell_pixels;
- p.update(.{ .resize = .{
- .cols = p.screen_w,
- .rows = p.screen_h,
- .cell_pixels = p.cell_pixels,
- } });
- try std.testing.expect(!pv.search_reveal_pending);
- p.update(.{ .resize = .{
- .cols = p.screen_w,
- .rows = p.screen_h,
- .cell_pixels = .{
- .w = original_cell_pixels.w,
- .h = original_cell_pixels.h + 1,
- },
- } });
- try std.testing.expect(pv.search_reveal_pending);
- _ = frame.reset(.retain_capacity);
- const geometry_changed = try p.render(frame.allocator());
- if (pdf_raster_policy.match_viewport)
- try std.testing.expect(geometry_changed.images[0].?.native.revision !=
- revision_before_geometry_change)
- else
- try std.testing.expectEqual(
- revision_before_geometry_change,
- geometry_changed.images[0].?.native.revision,
- );
- p.update(.{ .resize = .{
- .cols = p.screen_w,
- .rows = p.screen_h,
- .cell_pixels = original_cell_pixels,
- } });
- _ = frame.reset(.retain_capacity);
- _ = try p.render(frame.allocator());
-
- // Unsupported text-selection actions are consumed by the PDF adapter:
- // they neither invent a range over placeholder cells nor disturb the
- // native MuPDF selection. The select-button's no-drag click still clears.
- p.update(.{ .key = .{ .cp = 'v' } });
- try std.testing.expect(pv.selection != null);
- try std.testing.expect(!pane.vsel.active);
- p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = word_col, .row = selected_row } });
- p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = word_col, .row = selected_row } });
- try std.testing.expect(pv.selection == null);
- try std.testing.expect(pdf_pane.beginSelection(p, pane, word_col, selected_row));
-
- // Native drag events update MuPDF quads and copied text immediately, but
- // keep the already transmitted page generation stable until release.
- _ = frame.reset(.retain_capacity);
- const before_drag = try p.render(frame.allocator());
- const before_drag_revision = before_drag.images[0].?.native.revision;
- const drag_col = @min(r.x + r.w - 1, word_col +| 12);
- const drag_row = @min(r.y + r.h - 1, selected_row +| 4);
- p.update(.{ .mouse = .{
- .button = config.select_button,
- .kind = .press,
- .col = word_col,
- .row = selected_row,
- } });
- p.update(.{ .mouse = .{
- .button = config.select_button,
- .kind = .drag,
- .col = @min(drag_col, word_col +| 4),
- .row = @min(drag_row, selected_row +| 2),
- } });
- _ = frame.reset(.retain_capacity);
- const during_first_drag = try p.render(frame.allocator());
- try std.testing.expectEqual(
- before_drag_revision,
- during_first_drag.images[0].?.native.revision,
- );
- p.update(.{ .mouse = .{
- .button = config.select_button,
- .kind = .drag,
- .col = drag_col,
- .row = drag_row,
- } });
- try std.testing.expect(p.drag.select.pdf.selection_changed);
- _ = frame.reset(.retain_capacity);
- const during_second_drag = try p.render(frame.allocator());
- try std.testing.expectEqual(
- before_drag_revision,
- during_second_drag.images[0].?.native.revision,
- );
- p.update(.{ .mouse = .{
- .button = config.select_button,
- .kind = .release,
- .col = drag_col,
- .row = drag_row,
- } });
- try std.testing.expect(!raster.tried);
- _ = frame.reset(.retain_capacity);
- const committed_drag = try p.render(frame.allocator());
- try std.testing.expectEqual(
- before_drag_revision + 1,
- committed_drag.images[0].?.native.revision,
- );
- _ = frame.reset(.retain_capacity);
- const stable_drag = try p.render(frame.allocator());
- try std.testing.expectEqual(
- committed_drag.images[0].?.native.revision,
- stable_drag.images[0].?.native.revision,
- );
-
- const saved_query = try gpa.dupe(u8, pv.search_query);
- defer gpa.free(saved_query);
- pdf_pane.setPage(p, pane, 1);
- try std.testing.expect(pv.selection == null);
- try std.testing.expectEqual(@as(usize, 0), pv.selection_text.len);
- try std.testing.expectEqualStrings(saved_query, pv.search_query);
-}
-
-test "Esc back into a PDF keeps the offset within its page" {
- if (!pdf_enabled or platform == .web) return;
- const gpa = std.testing.allocator;
- var tmp = std.testing.tmpDir(.{});
- defer tmp.cleanup();
- const fixture = try pdf_impl.makeOutlineTestPdf(gpa);
- defer gpa.free(fixture);
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "outline.pdf", .data = fixture });
- var path_buf: [256]u8 = undefined;
- const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/outline.pdf", .{tmp.sub_path});
-
- const p = try Pardes.init(gpa, .{ .file = path, .cols = 80, .rows = 28 });
- defer p.deinit();
- p.update(.{ .resize = .{ .cols = 80, .rows = 28 } });
- const doc = p.active;
- const dp = p.panes[doc].?;
- try std.testing.expect(dp.pdf != null);
- pardes.pdf_test.sync(p);
-
- // Read a little way DOWN the page you are on, then step away.
- const pv = &dp.pdf.?;
- pv.document_scroll_y += 137;
- const mid = pv.document_scroll_y;
- pv.scroll_to_page_pending = false;
- p.update(.{ .key = .{ .cp = 'n', .alt = true } }); // a shell under the doc
- pardes.pdf_test.sync(p);
- const shell = p.active;
- try std.testing.expect(shell != doc);
-
- p.update(.{ .key = .{ .cp = Key.escape } }); // Esc: back into the PDF
- pardes.pdf_test.sync(p);
- try std.testing.expectEqual(doc, p.active);
-
- // The jumps stack records a PDF as its PAGE, so returning revealed the page
- // you were already on — and a reveal sets `document_scroll_y` to that page's
- // start, throwing away where you had read to inside it.
- try std.testing.expectEqual(mid, pv.document_scroll_y);
- // Same reveal, the other half: with no valid layout yet it only ARMS the
- // snap, so a test that watched the offset alone would not see it coming.
- try std.testing.expect(!pv.scroll_to_page_pending);
-}