All views be rendering big stuff

This commit is contained in:
Kilian Schuettler 2025-02-02 00:06:06 +01:00
parent 570d495faa
commit dcd1bd6d3f
16 changed files with 554 additions and 508 deletions

View File

@ -7,7 +7,7 @@ use crate::pdf::object::Resolve;
use lazy_static::lazy_static;
use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache};
use pdf::object::{Object, ObjectWrite, PlainRef, Stream};
use pdf::object::{Object, ObjectWrite, PlainRef, Stream, Trace};
use pdf::primitive::Primitive;
use pdf::xref::XRef;
use regex::Regex;
@ -62,6 +62,19 @@ pub struct PrimitiveModel {
pub expanded: bool,
}
#[derive(Serialize, Debug, Clone)]
pub struct PrimitiveTreeView {
pub depth: usize,
pub key: String,
pub ptype: String,
pub sub_type: String,
pub value: String,
pub container: bool,
pub expanded: bool,
pub path: Vec<PathTrace>,
pub active: bool,
}
#[derive(Serialize, Debug, Clone)]
pub struct PathTrace {
pub key: String,
@ -83,6 +96,7 @@ pub struct PageModel {
pub struct TreeViewRequest {
key: String,
children: Vec<TreeViewRequest>,
expand: bool,
}
impl TreeViewRequest {
@ -304,32 +318,45 @@ fn resolve_parent(step: Step, file: &CosFile) -> Result<(Primitive, PathTrace),
#[tauri::command]
fn get_prim_tree_by_path(
id: &str,
path: TreeViewRequest,
paths: Vec<TreeViewRequest>,
session: State<Mutex<Session>>,
) -> Result<PrimitiveModel, String> {
) -> Result<Vec<PrimitiveTreeView>, String> {
let session_guard = session
.lock()
.map_err(|_| "Failed to lock the session mutex.".to_string())?;
let file = get_file_from_state(id, &session_guard)?;
get_prim_tree_by_path_with_file(path, &file.cos_file)
let results = paths
.into_iter()
.map(|path| get_prim_tree_by_path_with_file(path, &file.cos_file))
.collect::<Result<Vec<_>, String>>()?
.into_iter()
.flatten()
.collect();
Ok(results)
}
fn get_prim_tree_by_path_with_file(
node: TreeViewRequest,
file: &CosFile,
) -> Result<PrimitiveModel, String> {
) -> Result<Vec<PrimitiveTreeView>, String> {
let step = node.step()?;
let (parent, trace) = resolve_parent(step.clone(), file)?;
let path = vec![trace];
let mut parent_model = PrimitiveModel::from_primitive_with_children(&parent, path);
let trace = vec![trace];
let mut parent_model: PrimitiveModel;
if node.expand {
parent_model = PrimitiveModel::from_primitive_with_children(&parent, trace);
for child in node.children.iter() {
expand(child, &mut parent_model, &parent, file)?;
for child in node.children.iter() {
expand(child, &mut parent_model, &parent, file)?;
}
} else {
parent_model = PrimitiveModel::from_primitive(step.get_key(), &parent, trace);
}
Ok(parent_model)
Ok(PrimitiveTreeView::flatten(0, parent_model))
}
fn expand(
@ -338,20 +365,21 @@ fn expand(
parent: &Primitive,
file: &CosFile,
) -> Result<(), String> {
if !node.expand {
return Ok(());
}
let step = node.step()?;
let prim = resolve_step(parent, &step)?;
if let Primitive::Reference(x_ref) = prim {
let jump = resolve_xref(x_ref.id, file)?;
// parent_model.ptype = format!("{}-Reference", jump.get_debug_name());
let mut jump_trace = parent_model.trace.clone();
jump_trace.push(PathTrace::new(step.get_key(), x_ref.id.to_string()));
let mut to_expand = parent_model.get_child(step.get_key()).unwrap();
to_expand.add_children(
&jump,
append_path_with_jump(step.get_key(), x_ref.id.to_string(), &to_expand.trace),
);
to_expand.add_children(&jump, &jump_trace);
expand_children(node, file, &jump, &mut to_expand)?;
} else {
let mut to_expand = parent_model.get_child(step.get_key()).unwrap();
to_expand.add_children(prim, append_path(step.get_key(), &to_expand.trace));
to_expand.add_children(prim, &to_expand.trace.clone());
expand_children(node, file, prim, &mut to_expand)?;
}
Ok(())
@ -426,6 +454,9 @@ fn retrieve_trailer(file: &CosFile) -> Primitive {
}
fn retrieve_page(page_num: u32, file: &CosFile) -> Result<(Primitive, PathTrace), String> {
if page_num <= 0 {
return Err("Page 0 does not exist, use 1-based index!".to_string());
}
let page_rc = t!(file.get_page(page_num - 1));
let p_ref = page_rc.get_ref().get_inner();
Ok((
@ -516,12 +547,6 @@ fn get_file_from_state<'a>(
.ok_or_else(|| format!("File with id {} does not exist!", id))
}
fn append_path_with_jump(key: String, last_jump: String, path: &Vec<PathTrace>) -> Vec<PathTrace> {
let mut new_path = path.clone();
new_path.push(PathTrace::new(key, last_jump));
new_path
}
fn append_path(key: String, path: &Vec<PathTrace>) -> Vec<PathTrace> {
let mut new_path = path.clone();
let last_jump = new_path.last().unwrap().last_jump.clone();
@ -541,7 +566,7 @@ impl PrimitiveModel {
Primitive::Dictionary(_) => "-".to_string(),
Primitive::Array(arr) => PrimitiveModel::format_arr_content(arr),
Primitive::Reference(pref) => {
format!("Obj Number: {} Gen Number: {}", pref.id, pref.gen)
format!("Obj Nr: {} Gen Nr: {}", pref.id, pref.gen)
}
Primitive::Name(name) => name.clone().as_str().to_string(),
@ -595,17 +620,20 @@ impl PrimitiveModel {
result
}
fn from_primitive_with_children(primitive: &Primitive, path: Vec<PathTrace>) -> PrimitiveModel {
fn from_primitive_with_children(
primitive: &Primitive,
trace: Vec<PathTrace>,
) -> PrimitiveModel {
let mut model = PrimitiveModel::from_primitive(
path.last().unwrap().key.clone(),
trace.last().unwrap().key.clone(),
primitive,
path.clone(),
trace.clone(),
);
model.add_children(primitive, path);
model.add_children(primitive, &trace);
model
}
fn add_children(&mut self, primitive: &Primitive, path: Vec<PathTrace>) {
fn add_children(&mut self, primitive: &Primitive, path: &Vec<PathTrace>) {
self.expanded = true;
match primitive {
Primitive::Dictionary(dict) => dict.iter().for_each(|(name, value)| {
@ -619,6 +647,13 @@ impl PrimitiveModel {
self.add_child(i.to_string(), obj, append_path(i.to_string(), &path));
}),
Primitive::Stream(stream) => {
stream.info.iter().for_each(|(name, value)| {
self.add_child(
name.clone().as_str().to_string(),
value,
append_path(name.clone().as_str().to_string(), &path),
);
});
self.children.push(PrimitiveModel {
key: "Data".to_string(),
ptype: "Stream Data".to_string(),
@ -628,13 +663,6 @@ impl PrimitiveModel {
trace: append_path("Data".to_string(), &path),
expanded: false,
});
stream.info.iter().for_each(|(name, value)| {
self.add_child(
name.clone().as_str().to_string(),
value,
append_path(name.clone().as_str().to_string(), &path),
);
})
}
_ => (),
};
@ -654,6 +682,42 @@ impl PrimitiveModel {
fn get_child(&mut self, key: String) -> Option<&mut PrimitiveModel> {
self.children.iter_mut().find(|child| child.key == key)
}
fn is_container(&self) -> bool {
self.ptype == "Dictionary"
|| self.ptype == "Array"
|| self.ptype == "Stream"
|| self.ptype == "Reference"
}
fn drain_children(&mut self) -> Vec<PrimitiveModel> {
self.children.drain(..).collect()
}
}
impl PrimitiveTreeView {
fn from_primitive(depth: usize, primitive: PrimitiveModel) -> PrimitiveTreeView {
let is_container = primitive.is_container();
PrimitiveTreeView {
depth: depth,
key: primitive.key,
ptype: primitive.ptype,
sub_type: primitive.sub_type,
value: primitive.value,
container: is_container,
expanded: primitive.expanded,
path: primitive.trace,
active: true,
}
}
fn flatten(depth: usize, mut primitive: PrimitiveModel) -> Vec<PrimitiveTreeView> {
let mut views: Vec<PrimitiveTreeView> = Vec::new();
let children = primitive.drain_children();
views.push(PrimitiveTreeView::from_primitive(depth, primitive));
children.into_iter().for_each(|child| {
views.extend(PrimitiveTreeView::flatten(depth + 1, child.clone()));
});
views
}
}
#[tauri::command]
fn get_xref_table(id: &str, session: State<Mutex<Session>>) -> Result<XRefTableModel, String> {
@ -686,7 +750,7 @@ fn get_xref_table_model_with_file(file: &CosFile) -> Result<XRefTableModel, Stri
}
XRef::Stream { stream_id, index } => XRefEntryModel {
obj_num: i as u64,
gen_num: *stream_id as u64,
gen_num: 0,
obj_type: "Stream".into(),
offset: *index as u64,
},

View File

@ -80,13 +80,26 @@ mod tests {
}],
});
let root = TreeViewRequest {
key: "/".to_string(),
key: "Trailer".to_string(),
children: path,
};
let message = format!("Retrieval of {:?}", root);
let prim = timed!(get_prim_tree_by_path_with_file(root, &file), message);
print_node(prim.unwrap(), 0);
let prim = timed!(get_prim_tree_by_path_with_file(root, &file), message).unwrap();
for ele in prim {
println!(
"{}{} | {} | {} | {:?}",
" ".repeat(ele.depth),
ele.key,
ele.ptype,
ele.value,
ele.path
.iter()
.map(|p| p.key.to_string())
.collect::<Vec<String>>()
.join("/")
);
}
}
#[test]
fn test_read_by_path() {
@ -94,7 +107,7 @@ mod tests {
FileOptions::cached().open(FILE_PATH).unwrap(),
"Loading file"
);
let path = "/Root/Pages";
let path = "/Trailer/Root/Pages";
let message = format!("Retrieval of {:?}", path);
let prim = timed!(get_prim_model_by_path_with_file(path, &file), message);
@ -122,9 +135,8 @@ mod tests {
"writing trailer"
);
let trail_model = PrimitiveModel::from_primitive_with_children(
"Trailer".to_string(),
&trail,
vec![PathTrace::new("/".to_string(), "/".to_string())],
vec![PathTrace::new("Trailer".to_string(), "Trailer".to_string())],
);
print_node(trail_model, 5);
println!("{:?}", file.trailer.info_dict);

View File

@ -34,21 +34,31 @@
loadContents(path, id);
});
$inspect(contents);
function loadContents(path: string | undefined, id: string) {
console.log("Loading contents for", path, id);
if (!path || !id) return;
invoke<ContentModel>("get_contents", { id, path })
.then((result) => {
console.log("Contents loaded", result);
contents = result;
if (contents && editor) {
const text = contents.parts
.map((part) => part.join("\n"))
.join(
"\n\n%-------------------% EOF %-------------------%\n\n",
);
let text = "";
if (contents.parts.length > 1) {
let i = 0;
for (let part of contents.parts) {
text +=
"%----------------% Contents[" +
i +
"] %--------------%\n\n";
for (let line of part) {
text += " " + line + "\n";
}
text +=
"\n%-------------------% EOF %-------------------%\n\n";
i++;
}
} else {
text = contents.parts[0].join("\n");
}
editor.setValue(text);
}
})

View File

@ -4,13 +4,9 @@
import PrimitiveView from "./PrimitiveView.svelte";
import TreeView from "./TreeView.svelte";
import type FileViewState from "../models/FileViewState.svelte";
import { createEventDispatcher, onMount } from "svelte";
import PageList from "./PageList.svelte";
import ContentsView from "./ContentsView.svelte";
import TreeViewRequest from "../models/TreeViewRequest.svelte";
import TreeViewState from "../models/TreeViewState.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
TreeViewRequest;
import { onMount } from "svelte";
let {
treeShowing,
xrefTableShowing,
@ -68,19 +64,7 @@
minSize={treeShowing || pagesShowing ? 2 : 0}
maxSize={treeShowing || pagesShowing ? 100 : 0}
>
{#if pagesShowing}
<PageList {fState} h={height}></PageList>
{:else if treeShowing}
<div class="overflow-auto" style="height: {height}px">
<TreeView
{pathSelectedHandler}
file_id={fState.file.id}
root={TreeViewRequest.TRAILER}
active={true}
details={false}
></TreeView>
</div>
{/if}
<TreeView {fState} {height}></TreeView>
</Pane>
<Pane minSize={1}>

View File

@ -1,70 +0,0 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import TreeView from "./TreeView.svelte";
import TreeViewRequest from "../models/TreeViewRequest.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
let { fState, h }: { fState: FileViewState; h: number } = $props();
let selected_page_num: number | undefined = $state(undefined);
let pages = $derived(fState.file.pages);
function pathSelectedHandler(event: PathSelectedEvent) {
let path = event.detail.path;
if (
path.length > 0 &&
path[0].startsWith("Page") &&
!isNaN(+path[0].replace("Page", ""))
) {
selected_page_num = +path[0].replace("Page", "");
}
fState.selectPath(path);
}
</script>
<div class="overflow-x-auto">
<div class="overflow-y-auto" style="height: {h}px">
{#each pages as page}
<TreeView
file_id={fState.file.id}
root={new TreeViewRequest("Page" + page.page_num, [])}
{pathSelectedHandler}
active={selected_page_num === page.page_num}
details={true}
></TreeView>
{/each}
</div>
</div>
<style lang="postcss">
.key-field {
display: flex;
flex-direction: row;
align-items: center;
gap: 3px;
}
.selected {
@apply bg-forge-acc;
}
.t-header {
@apply uppercase text-xs text-forge-text p-5 bg-forge-sec border-r;
cursor: default;
}
.t-data {
@apply border border-forge-sec text-xs text-forge-text text-left;
text-align: left;
cursor: default;
}
.ref-cell {
@apply min-w-[100px] w-[100px] min-h-[20px] h-[20px] p-1 m-0;
user-select: none;
}
.page-cell {
@apply min-w-[150px] w-[150px] min-h-[20px] h-[20px] p-1 m-0;
user-select: none;
}
</style>

View File

@ -1,35 +1,35 @@
<script lang="ts">
import {
FileOutline,
FolderArrowRightOutline,
FolderOutline,
CodeOutline,
} from "flowbite-svelte-icons";
import {FileOutline, FolderArrowRightOutline, FolderOutline, CodeOutline} from "flowbite-svelte-icons";
let {ptype}: {ptype: string} = $props()
let { ptype }: { ptype: string } = $props();
</script>
{#if ptype === "Dictionary"}
<FolderOutline class="stroke-blue-300 text-blue-300 primitive-icon"/>
{:else if ptype === "Array"}
<FolderOutline class=" text-orange-300 primitive-icon "/>
{:else if ptype === "Reference"}
<FolderArrowRightOutline class=" text-purple-300 primitive-icon"/>
{:else if ptype === "Integer"}
<FileOutline class="text-pink-300 primitive-icon"/>
{:else if ptype === "Number"}
<FileOutline class="text-lime-300 primitive-icon"/>
{:else if ptype === "Boolean"}
<FileOutline class="text-fuchsia-300 primitive-icon"/>
{:else if ptype === "String"}
<FileOutline class="text-green-300 primitive-icon"/>
{:else if ptype === "Name"}
<FileOutline class="text-red-400 primitive-icon"/>
{:else if ptype === "Stream Data"}
<CodeOutline class="text-purple-400 primitive_icon"/>
{:else}
<FileOutline/>
{/if}
{#if ptype === "Dictionary"}
<FolderOutline class="stroke-blue-300 text-blue-300 primitive-icon" />
{:else if ptype === "Array"}
<FolderOutline class=" text-orange-300 primitive-icon " />
{:else if ptype === "Reference"}
<FolderArrowRightOutline class=" text-purple-300 primitive-icon" />
{:else if ptype === "Integer"}
<FileOutline class="text-pink-300 primitive-icon" />
{:else if ptype === "Number"}
<FileOutline class="text-lime-300 primitive-icon" />
{:else if ptype === "Boolean"}
<FileOutline class="text-fuchsia-300 primitive-icon" />
{:else if ptype === "String"}
<FileOutline class="text-green-300 primitive-icon" />
{:else if ptype === "Name"}
<FileOutline class="text-red-400 primitive-icon" />
{:else if ptype === "Stream Data"}
<CodeOutline class="text-purple-400 primitive_icon" />
{:else}
<FileOutline />
{/if}
<style lang="postcss">
.primitive_icon {
margin: auto;
}
</style>
</style>

View File

@ -5,12 +5,22 @@
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import StreamEditor from "./StreamEditor.svelte";
const cellH = 29;
const headerOffset = 24;
let { fState, height }: { fState: FileViewState; height: number } =
$props();
let bodyHeight = $derived(height - 24);
let fillerHeight: number = $state(0);
let firstEntry = $state(0);
let lastEntry = $state(100);
let scrollY = $state(0);
let prim = $derived(fState.prim);
let showContents = $state(false);
let tableHeight = $state(0);
let entriesToDisplay: Primitive[] = $derived(
prim ? prim.children.slice(firstEntry, lastEntry) : [],
);
let tableHeight = $derived(prim ? prim.children.length * cellH : 0);
let bodyHeight = $derived(height - headerOffset);
let editorHeight = $derived(Math.max(800, bodyHeight - tableHeight));
$inspect(fState.highlightedPrim);
function handlePrimSelect(prim: Primitive) {
@ -25,6 +35,14 @@
}
return;
}
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
scrollY = event.currentTarget.scrollTop;
firstEntry = Math.floor(scrollY / cellH);
lastEntry = Math.ceil((scrollY + bodyHeight) / cellH);
fillerHeight = firstEntry * cellH;
}
</script>
{#if prim && prim.children && prim.children.length > 0}
@ -41,31 +59,44 @@
</tr>
</thead>
</table>
<div class="overflow-y-auto" style="height: {bodyHeight}px">
<table bind:clientHeight={tableHeight}>
<tbody>
{#each prim.children as entry}
<tr
class:selected={entry.key ===
fState.highlightedPrim?.key}
class="hover:bg-forge-sec"
onclick={() => (fState.highlightedPrim = entry)}
ondblclick={() => handlePrimSelect(entry)}
>
<td class="page-cell t-data">
<div class="key-field">
<PrimitiveIcon ptype={entry.ptype} />
<p class="text-left">
{entry.key}
</p>
</div>
</td>
<td class="ref-cell t-data">{entry.ptype}</td>
<td class="cell t-data">{entry.value}</td>
</tr>
{/each}
</tbody>
</table>
<div
onscroll={handleScroll}
class="overflow-y-auto"
style="height: {bodyHeight}px"
>
<div class="container" style="height: {tableHeight}px">
<table>
<tbody>
<tr class="filler" style="height: {fillerHeight}px"
></tr>
{#each entriesToDisplay as entry}
<tr
class:selected={entry.key ===
fState.highlightedPrim?.key}
class="row"
onclick={() =>
(fState.highlightedPrim = entry)}
ondblclick={() => handlePrimSelect(entry)}
>
<td class="page-cell t-data">
<div class="key-field">
<PrimitiveIcon
ptype={entry.ptype}
/>
<p class="text-left">
{entry.key}
</p>
</div>
</td>
<td class="ref-cell t-data"
>{entry.ptype}</td
>
<td class="cell t-data">{entry.value}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#if fState.prim?.ptype === "Stream"}
<StreamEditor
fileId={fState.file.id}
@ -86,20 +117,11 @@
gap: 3px;
}
.path-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 3px;
}
.path-view {
@apply bg-forge-dark border border-forge-bound text-sm font-extralight mb-2 mt-2 rounded-sm;
flex: 1;
display: flex;
flex-direction: row;
user-select: auto;
.row {
@apply hover:bg-forge-sec;
height: 29px;
min-height: 29px;
max-height: 29px;
}
.selected {
@ -118,17 +140,17 @@
}
.cell {
@apply min-w-[600px] w-[600px] min-h-[15px] h-[15px] p-1 m-0;
@apply min-w-[600px] w-[600px] p-1 m-0;
user-select: none;
}
.ref-cell {
@apply min-w-[100px] w-[100px] min-h-[15px] h-[15px] p-1 m-0;
@apply min-w-[100px] w-[100px] p-1 m-0;
user-select: none;
}
.page-cell {
@apply min-w-[150px] w-[150px] min-h-[15px] h-[15px] p-1 m-0;
@apply min-w-[150px] w-[150px] p-1 m-0;
user-select: none;
}
</style>

View File

@ -33,14 +33,11 @@
loadContents(path, fileId);
});
$inspect(contents);
function loadContents(path: string | undefined, id: string) {
console.log("Loading contents for", path, id);
if (!path || !id) return;
path = path + "/Data";
invoke<string>("get_stream_data", { id, path })
.then((result) => {
console.log("Contents loaded", result);
contents = result;
if (contents && editor) {
editor.setValue(contents);

View File

@ -1,167 +0,0 @@
<script lang="ts">
import type Primitive from "../models/Primitive.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import { CaretRightOutline, CaretDownOutline } from "flowbite-svelte-icons";
import type TreeViewState from "../models/TreeViewState.svelte";
import { PathSelectedEvent } from "../events/PathSelectedEvent";
import { createEventDispatcher } from "svelte";
let {
file_id,
parent_view,
parent_path,
treeState,
pathSelectedHandler = $bindable(),
details,
}: {
file_id: string;
parent_view: Primitive | undefined;
parent_path: string[];
treeState: TreeViewState | undefined;
pathSelectedHandler: any;
details: boolean;
} = $props();
function copyPathAndAppend(key: string): string[] {
const _path = copyPath();
_path.push(key);
return _path;
}
function copyPath(): string[] {
const _path: string[] = [];
for (let item of parent_path) {
_path.push(item);
}
return _path;
}
function selectItem(child: Primitive) {
let _path = copyPathAndAppend(child.key);
if (child.expanded) {
treeState?.collapseTree(_path);
} else {
treeState?.expandTree(_path);
}
const event = new PathSelectedEvent(file_id, _path);
pathSelectedHandler(event);
}
</script>
{#if parent_view}
{#each parent_view.children as child}
<button
class="row hover:bg-forge-sec w-full"
onclick={() => selectItem(child)}
>
<div class="item" style="margin-left: {parent_path.length * 1}em">
{#if child.isContainer()}
<div>
<span class="caret"
>{#if child.expanded}<CaretDownOutline
/>{:else}<CaretRightOutline />{/if}</span
>
</div>
{:else}
<span class="no-caret"></span>
{/if}
<div class="item">
<PrimitiveIcon ptype={child.ptype} />
<div class="row">
<p>
{child.key}
</p>
{#if details}
<p class="hint">
{" | " +
child.ptype +
" | " +
child.sub_type +
" | " +
child.value}
</p>
{:else}
<p class="ml-1 text-forge-sec small">
{child.sub_type}
</p>
{/if}
</div>
</div>
</div>
</button>
{#if child.children.length > 0}
<svelte:self
{file_id}
parent_view={child}
parent_path={copyPathAndAppend(child.key)}
{treeState}
bind:pathSelectedHandler
{details}
></svelte:self>
{/if}
{/each}
{/if}
<style lang="postcss">
.hint {
@apply ml-1 text-forge-text_hint text-xs whitespace-nowrap font-extralight;
}
.select-button {
@apply hover:bg-forge-sec pr-3;
cursor: pointer;
}
.item {
@apply text-sm rounded;
text-align: center;
display: flex;
flex-direction: row;
user-select: none;
}
.small {
display: flex;
justify-content: center;
align-items: center;
}
.row {
@apply ml-1;
text-align: center;
display: flex;
flex-direction: row;
user-select: none;
}
/* Remove default bullets */
ul,
#myUL {
list-style-type: none;
}
ul {
@apply pl-5;
}
.no-caret {
@apply pl-5;
user-select: none;
}
.caret {
@apply text-forge-sec;
cursor: pointer;
user-select: none;
}
.caret-active {
transform: rotate(90deg);
}
/* Show the nested list when the user clicks on the caret/arrow (with JavaScript) */
.active {
display: block;
}
</style>

View File

@ -1,80 +1,148 @@
<script lang="ts">
import TreeNode from "./TreeNode.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import TreeViewState from "../models/TreeViewState.svelte";
import type TreeViewRequest from "../models/TreeViewRequest.svelte";
import TreeViewRequest from "../models/TreeViewRequest.svelte";
import { PathSelectedEvent } from "../events/PathSelectedEvent";
import { CaretDownOutline, CaretRightOutline } from "flowbite-svelte-icons";
import type { PrimitiveView } from "../models/PrimitiveView";
import { onMount } from "svelte";
import type FileViewState from "../models/FileViewState.svelte";
const rowHeight = 24;
let {
file_id,
root,
pathSelectedHandler,
active,
details,
fState,
height,
}: {
file_id: string;
root: TreeViewRequest;
pathSelectedHandler: any;
active: boolean;
details: boolean;
fState: FileViewState;
height: number;
} = $props();
let h = $state(100);
let states: TreeViewState[] = $state([]);
let treeState: TreeViewState | undefined = $derived(
states.find((state) => state.file_id === file_id),
);
const file_id = $derived(fState.file.id);
if (active) {
loadTreeView();
}
let fillerHeight: number = $state(0);
let states: TreeViewState[] = [];
let firstEntry = $state(0);
let lastEntry = $state(100);
let scrollY = $state(0);
let entryCount = $state(0);
function loadTreeView() {
if (!treeState) {
let newState = new TreeViewState(file_id, root);
newState.loadTreeView();
let treeState: TreeViewState | undefined = $state(undefined);
let totalHeight: number = $state(100);
let entries: PrimitiveView[] | undefined = $state([]);
$effect(() => {
treeState = states.find((state) => state.file_id === file_id);
updateTreeView();
});
async function updateTreeView() {
if (!treeState || treeState.file_id !== file_id) {
let newState = new TreeViewState(file_id, fState);
await newState.loadTreeView();
states.push(newState);
treeState = newState;
totalHeight = treeState.getEntryCount() * rowHeight;
}
firstEntry = Math.floor(scrollY / rowHeight);
lastEntry = Math.ceil((scrollY + height) / rowHeight);
entryCount = treeState?.getEntryCount();
totalHeight = Math.max(entryCount * rowHeight, 0);
fillerHeight = firstEntry * rowHeight;
entries = treeState.getEntries(firstEntry, lastEntry);
}
function select() {
loadTreeView();
pathSelectedHandler(new PathSelectedEvent(file_id, [root.key]));
function handleSelect(prim: PrimitiveView) {
if (prim.expanded && prim.container) {
treeState
?.collapseTree(prim.path.map((path) => path.key))
.then(() => {
updateTreeView();
});
return;
} else if (prim.container) {
treeState
?.expandTree(prim.path.map((path) => path.key))
.then(() => {
updateTreeView();
});
}
fState.selectPathHandler(
new PathSelectedEvent(
file_id,
prim.path.map((path) => path.key),
),
);
updateTreeView();
}
function formatDisplayKey(key: string) {
if (key.startsWith("Page") && !key.startsWith("Pages")) {
return key.replace("Page", "Page ");
}
return key;
}
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
scrollY = event.currentTarget.scrollTop;
updateTreeView();
}
</script>
<ul id="myUL">
{#if root}
<li>
<button
class="item hover:bg-forge-sec w-full"
onmouseenter={() => loadTreeView()}
onclick={() => select()}
>
{#if active && treeState?.view}
<span class="caret"><CaretDownOutline /></span>
{:else}
<span class="caret"><CaretRightOutline /></span>
<div onscroll={handleScroll} class="overflow-auto" style="height: {height}px">
{#if entries}
<div style="height: {totalHeight}px; width: 100%">
<div
class="filler"
style="height: {fillerHeight}px; width: 100%"
></div>
{#each entries as entry}
{#if entry.active}
{#if entry.depth == 0}
<div
style="height: 1px; width: 100%;"
class="bg-forge-bound"
></div>
{/if}
<button
class="row text-sm hover:bg-forge-sec w-full group whitespace-nowrap"
style="height: {rowHeight}px;"
onclick={() => handleSelect(entry)}
>
<div style="margin-left: {entry.depth * 1.25}em">
{#if entry.container}
<div>
<span
class="caret group-hover:text-forge-text_hint"
>{#if entry.expanded}<CaretDownOutline
/>{:else}<CaretRightOutline
/>{/if}</span
>
</div>
{:else}
<span class="no-caret"></span>
{/if}
</div>
<PrimitiveIcon ptype={entry.ptype} />
<p class="pl-1">
{formatDisplayKey(entry.key)}
</p>
<p
class="details group-hover:text-forge-text_hint whitespace-nowrap"
>
{" | " +
entry.value +
" | " +
entry.sub_type +
" | " +
entry.ptype}
</p>
</button>
{/if}
<div class="item">
<PrimitiveIcon ptype={"Dictionary"} />
{root.key}
</div>
</button>
{#if active && treeState?.view}
<TreeNode
{file_id}
bind:pathSelectedHandler
parent_view={treeState.view}
parent_path={treeState.view.getPath()}
{treeState}
{details}
></TreeNode>
{/if}
</li>
{/each}
</div>
{/if}
</ul>
</div>
<style lang="postcss">
.item {
@ -83,14 +151,41 @@
display: flex;
flex-direction: row;
}
ul,
#myUL {
list-style-type: none;
}
/* Style the caret/arrow */
.caret {
@apply text-forge-sec;
cursor: pointer;
user-select: none; /* Prevent text selection */
user-select: none;
}
.details {
@apply ml-1 text-forge-prim text-xs whitespace-nowrap font-extralight;
text-align: right;
}
.item {
@apply text-sm rounded;
text-align: center;
display: flex;
flex-direction: row;
user-select: none;
}
.small {
display: flex;
justify-content: center;
align-items: center;
}
.row {
text-align: center;
display: flex;
flex-direction: row;
user-select: none;
}
.no-caret {
@apply pl-5;
user-select: none;
}
</style>

View File

@ -84,10 +84,10 @@
<td class="cell t-header border-forge-sec">Offset</td>
</tr>
<tr
class={selectedPath === "/"
class={selectedPath === "Trailer"
? "bg-forge-acc"
: "hover:bg-forge-sec"}
ondblclick={() => fState.selectXref(undefined)}
onclick={() => fState.selectXref(undefined)}
>
<td class="cell t-data">Trailer</td>
<td class="cell t-data">65535</td>
@ -112,7 +112,7 @@
class={selectedPath === entry.obj_num
? "bg-forge-acc"
: "hover:bg-forge-sec"}
ondblclick={() => fState.selectXref(entry)}
onclick={() => fState.selectXref(entry)}
>
<td class="cell t-data">{entry.obj_num}</td>
<td class="cell t-data">{entry.gen_num}</td>
@ -149,15 +149,4 @@
.scrollContainer {
overflow-y: auto;
}
.xref-modal {
position: fixed;
right: 0;
top: 0;
bottom: 0;
width: 281px;
background: var(--background-color);
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
z-index: 1000;
overflow-y: auto;
}
</style>

View File

@ -55,7 +55,7 @@ export default class FileViewState {
}
public selectPathHandler(event: PathSelectedEvent) {
console.log("Selecting path", event.detail);
if (event.detail.file_id !== this.file.id) {
return;
}

View File

@ -0,0 +1,15 @@
import type { Trace } from "./Primitive.svelte";
export class PrimitiveView {
constructor(
public depth: number,
public key: string,
public ptype: string,
public sub_type: string,
public value: string,
public container: boolean,
public expanded: boolean,
public path: Trace[],
public active: boolean,
) { }
}

View File

@ -3,11 +3,12 @@ export default class TreeViewRequest {
public key: string;
public children: TreeViewRequest[];
public displayName: string;
public active: boolean;
expand: boolean;
constructor(
key: string,
children: TreeViewRequest[]
children: TreeViewRequest[],
expand: boolean = false,
) {
if (key.startsWith("Page")) {
this.displayName = "Page " + key.slice(4);
@ -16,13 +17,21 @@ export default class TreeViewRequest {
}
this.key = key;
this.children = children;
this.active = true;
this.expand = expand;
}
static TRAILER = new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [])]);
static fromPageCount(pageCount: number) {
let roots = [TreeViewRequest.TRAILER];
for (let i = 0; i < pageCount; i++) {
roots.push(new TreeViewRequest("Page" + (i + 1), []));
}
return roots;
}
static TRAILER = new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [], true)], true);
public clone(): TreeViewRequest {
return new TreeViewRequest(this.key, this.children.map(child => child.clone()));
return new TreeViewRequest(this.key, this.children.map(child => child.clone()), this.expand);
}
public getChild(key: string) {
@ -30,16 +39,17 @@ export default class TreeViewRequest {
}
public addChild(key: string) {
let child = new TreeViewRequest(key, [])
this.expand = true;
let child = new TreeViewRequest(key, [], true)
this.children.push(child);
return child;
}
public clearChildren() {
this.children = [];
}
public removeChild(key: string) {
this.children = this.children.filter(child => child.key !== key);
}
}
public setExpand(expand: boolean) {
this.expand = expand;
}
}

View File

@ -1,79 +1,164 @@
import { invoke } from "@tauri-apps/api/core";
import Primitive from "./Primitive.svelte";
import TreeViewRequest from "./TreeViewRequest.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
import { PrimitiveView } from "./PrimitiveView";
import type { Trace } from "./Primitive.svelte";
import type FileViewState from "./FileViewState.svelte";
export default class TreeViewState {
private root: TreeViewRequest = $state(new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [])]));
public view: Primitive | undefined = $state();
active: boolean = $state(false);
private request: TreeViewRequest[];
private activeRequest: Map<string, TreeViewRequest> = $state(new Map());
private activeEntries: Map<string, PrimitiveView[]> = $state(new Map());
file_id: string;
constructor(file_id: string, root: TreeViewRequest) {
console.log("Creating tree view state", file_id, root);
constructor(file_id: string, fState: FileViewState) {
this.request = TreeViewRequest.fromPageCount(+fState.file.page_count);
this.activeRequest.set(this.request[0].key, this.request[0]);
this.file_id = file_id;
this.root = root;
}
public loadTreeView() {
this.setTreeViewRequest(this.getRoot());
public getEntryCount() {
let count = 0;
this.activeEntries.forEach((value, key) => {
count += value.length - 1;
});
return this.request.length + count;
}
public getRoot(): TreeViewRequest {
return this.root.clone();
public getEntries(start: number, end: number): PrimitiveView[] {
let i = 0;
let result: PrimitiveView[] = [];
for (let request of this.request) {
if (this.activeEntries.has(request.key)) {
let entries = this.activeEntries.get(request.key);
for (let entry of entries) {
if (i >= start) {
result.push(entry);
}
i += 1;
if (i >= end) {
console.log("end", i, end);
return result;
}
}
} else {
if (i >= start) {
result.push(
new PrimitiveView(
0,
request.key,
"Dictionary",
"-",
"-",
true,
false,
[{ key: request.key, last_jump: request.key } as Trace],
true,
))
}
i += 1;
if (i >= end) {
console.log("end", i, end);
return result;
}
}
}
return result;
}
public setTreeViewRequest(treeViewRequest: TreeViewRequest) {
console.log("Loading tree view", treeViewRequest);
invoke<Primitive>("get_prim_tree_by_path", {
public toView(request: TreeViewRequest): PrimitiveView {
return new PrimitiveView(
0,
request.key,
"Dictionary",
"-",
"-",
true,
false,
[{ key: request.key, last_jump: request.key } as Trace],
false,
);
}
public async loadTreeView() {
const activeRequests = Array.from(this.activeRequest.values());
await this.updateTreeViewRequest(activeRequests);
}
public getRoot(): TreeViewRequest[] {
let _roots: TreeViewRequest[] = [];
this.request.forEach((root) => {
_roots.push(root.clone());
});
return _roots;
}
public async updateTreeViewRequest(treeViewRequests: TreeViewRequest[]) {
let result = await invoke<PrimitiveView[]>("get_prim_tree_by_path", {
id: this.file_id,
path: treeViewRequest,
})
.then((result) => {
this.view = new Primitive(result);
this.root = treeViewRequest;
})
.catch((err) => console.error(err));
paths: treeViewRequests,
});
for (let i = 0; i < treeViewRequests.length; i++) {
let request = treeViewRequests[i];
if (request.expand) {
this.activeRequest.set(request.key, request);
} else {
this.activeRequest.delete(request.key);
}
let prim = result.filter(r => r.path[0].key == request.key);
if (prim) {
this.activeEntries.set(request.key, prim);
}
}
}
public expandTree(path: string[]) {
public async expandTree(path: string[]) {
if (path.length == 0) {
console.error("Empty path");
return;
}
console.log("Expanding tree", this.getRoot(), path);
let root = this.getRoot();
let root = this.activeRequest.get(path[0]);
if (!root) {
root = this.getRoot().find((root) => root.key === path[0]);
if (!root) {
console.error("Root not found for path: " + path);
return;
}
}
root.setExpand(true);
let node = root;
for (let key of path.slice(1, path.length)) {
let _node: TreeViewRequest | undefined = node.getChild(key);
if (_node) {
_node.setExpand(true);
node = _node;
} else {
node = node.addChild(key);
}
}
this.setTreeViewRequest(root);
await this.updateTreeViewRequest([root]);
}
public collapseTree(path: string[]) {
public async collapseTree(path: string[]) {
console.log("collapseTree", path);
if (path.length == 0) {
console.error("Empty path");
return;
}
if (path.length == 1) {
this.view = this.view?.withoutChildren();
let root: TreeViewRequest | undefined = this.activeRequest.get(path[0]);
if (!root) {
console.error("Root not found for path: " + path);
return;
}
let root: TreeViewRequest = this.root;
let node: TreeViewRequest | undefined = root;
for (let key of path.slice(1, path.length - 1)) {
for (let key of path.slice(1, path.length)) {
console.log("collapseTree", key);
node = node?.getChild(key);
}
if (node) {
node.removeChild(path[path.length - 1]);
this.setTreeViewRequest(root);
node.setExpand(false);
await this.updateTreeViewRequest([root]);
}
}
}

View File

@ -28,7 +28,7 @@ export default {
bound: 'rgba(0, 0, 0, 0.29)',
text: '#dadada',
text_sec: '#838686',
text_hint: '#5adada',
text_hint: '#5cc4c4c2',
}
}
}