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

View File

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

View File

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

View File

@ -4,13 +4,9 @@
import PrimitiveView from "./PrimitiveView.svelte"; import PrimitiveView from "./PrimitiveView.svelte";
import TreeView from "./TreeView.svelte"; import TreeView from "./TreeView.svelte";
import type FileViewState from "../models/FileViewState.svelte"; import type FileViewState from "../models/FileViewState.svelte";
import { createEventDispatcher, onMount } from "svelte";
import PageList from "./PageList.svelte";
import ContentsView from "./ContentsView.svelte"; import ContentsView from "./ContentsView.svelte";
import TreeViewRequest from "../models/TreeViewRequest.svelte";
import TreeViewState from "../models/TreeViewState.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent"; import type { PathSelectedEvent } from "../events/PathSelectedEvent";
TreeViewRequest; import { onMount } from "svelte";
let { let {
treeShowing, treeShowing,
xrefTableShowing, xrefTableShowing,
@ -68,19 +64,7 @@
minSize={treeShowing || pagesShowing ? 2 : 0} minSize={treeShowing || pagesShowing ? 2 : 0}
maxSize={treeShowing || pagesShowing ? 100 : 0} maxSize={treeShowing || pagesShowing ? 100 : 0}
> >
{#if pagesShowing} <TreeView {fState} {height}></TreeView>
<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}
</Pane> </Pane>
<Pane minSize={1}> <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,10 +1,14 @@
<script lang="ts"> <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> </script>
{#if ptype === "Dictionary"} {#if ptype === "Dictionary"}
<FolderOutline class="stroke-blue-300 text-blue-300 primitive-icon" /> <FolderOutline class="stroke-blue-300 text-blue-300 primitive-icon" />
{:else if ptype === "Array"} {:else if ptype === "Array"}
@ -26,10 +30,6 @@
{:else} {:else}
<FileOutline /> <FileOutline />
{/if} {/if}
<style lang="postcss"> <style lang="postcss">
.primitive_icon {
margin: auto;
}
</style> </style>

View File

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

View File

@ -33,14 +33,11 @@
loadContents(path, fileId); loadContents(path, fileId);
}); });
$inspect(contents);
function loadContents(path: string | undefined, id: string) { function loadContents(path: string | undefined, id: string) {
console.log("Loading contents for", path, id);
if (!path || !id) return; if (!path || !id) return;
path = path + "/Data"; path = path + "/Data";
invoke<string>("get_stream_data", { id, path }) invoke<string>("get_stream_data", { id, path })
.then((result) => { .then((result) => {
console.log("Contents loaded", result);
contents = result; contents = result;
if (contents && editor) { if (contents && editor) {
editor.setValue(contents); 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"> <script lang="ts">
import TreeNode from "./TreeNode.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte"; import PrimitiveIcon from "./PrimitiveIcon.svelte";
import TreeViewState from "../models/TreeViewState.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 { PathSelectedEvent } from "../events/PathSelectedEvent";
import { CaretDownOutline, CaretRightOutline } from "flowbite-svelte-icons"; 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 { let {
file_id, fState,
root, height,
pathSelectedHandler,
active,
details,
}: { }: {
file_id: string; fState: FileViewState;
root: TreeViewRequest; height: number;
pathSelectedHandler: any;
active: boolean;
details: boolean;
} = $props(); } = $props();
let h = $state(100); const file_id = $derived(fState.file.id);
let states: TreeViewState[] = $state([]);
let treeState: TreeViewState | undefined = $derived(
states.find((state) => state.file_id === file_id),
);
if (active) { let fillerHeight: number = $state(0);
loadTreeView(); let states: TreeViewState[] = [];
} let firstEntry = $state(0);
let lastEntry = $state(100);
let scrollY = $state(0);
let entryCount = $state(0);
function loadTreeView() { let treeState: TreeViewState | undefined = $state(undefined);
if (!treeState) {
let newState = new TreeViewState(file_id, root); let totalHeight: number = $state(100);
newState.loadTreeView(); 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); 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() { function handleSelect(prim: PrimitiveView) {
loadTreeView(); if (prim.expanded && prim.container) {
pathSelectedHandler(new PathSelectedEvent(file_id, [root.key])); 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> </script>
<ul id="myUL"> <div onscroll={handleScroll} class="overflow-auto" style="height: {height}px">
{#if root} {#if entries}
<li> <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 <button
class="item hover:bg-forge-sec w-full" class="row text-sm hover:bg-forge-sec w-full group whitespace-nowrap"
onmouseenter={() => loadTreeView()} style="height: {rowHeight}px;"
onclick={() => select()} 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
> >
{#if active && treeState?.view}
<span class="caret"><CaretDownOutline /></span>
{:else}
<span class="caret"><CaretRightOutline /></span>
{/if}
<div class="item">
<PrimitiveIcon ptype={"Dictionary"} />
{root.key}
</div> </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> </button>
{#if active && treeState?.view}
<TreeNode
{file_id}
bind:pathSelectedHandler
parent_view={treeState.view}
parent_path={treeState.view.getPath()}
{treeState}
{details}
></TreeNode>
{/if} {/if}
</li> {/each}
</div>
{/if} {/if}
</ul> </div>
<style lang="postcss"> <style lang="postcss">
.item { .item {
@ -83,14 +151,41 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
ul,
#myUL {
list-style-type: none;
}
/* Style the caret/arrow */
.caret { .caret {
@apply text-forge-sec; @apply text-forge-sec;
cursor: pointer; 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> </style>

View File

@ -84,10 +84,10 @@
<td class="cell t-header border-forge-sec">Offset</td> <td class="cell t-header border-forge-sec">Offset</td>
</tr> </tr>
<tr <tr
class={selectedPath === "/" class={selectedPath === "Trailer"
? "bg-forge-acc" ? "bg-forge-acc"
: "hover:bg-forge-sec"} : "hover:bg-forge-sec"}
ondblclick={() => fState.selectXref(undefined)} onclick={() => fState.selectXref(undefined)}
> >
<td class="cell t-data">Trailer</td> <td class="cell t-data">Trailer</td>
<td class="cell t-data">65535</td> <td class="cell t-data">65535</td>
@ -112,7 +112,7 @@
class={selectedPath === entry.obj_num class={selectedPath === entry.obj_num
? "bg-forge-acc" ? "bg-forge-acc"
: "hover:bg-forge-sec"} : "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.obj_num}</td>
<td class="cell t-data">{entry.gen_num}</td> <td class="cell t-data">{entry.gen_num}</td>
@ -149,15 +149,4 @@
.scrollContainer { .scrollContainer {
overflow-y: auto; 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> </style>

View File

@ -55,7 +55,7 @@ export default class FileViewState {
} }
public selectPathHandler(event: PathSelectedEvent) { public selectPathHandler(event: PathSelectedEvent) {
console.log("Selecting path", event.detail);
if (event.detail.file_id !== this.file.id) { if (event.detail.file_id !== this.file.id) {
return; 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 key: string;
public children: TreeViewRequest[]; public children: TreeViewRequest[];
public displayName: string; public displayName: string;
public active: boolean; expand: boolean;
constructor( constructor(
key: string, key: string,
children: TreeViewRequest[] children: TreeViewRequest[],
expand: boolean = false,
) { ) {
if (key.startsWith("Page")) { if (key.startsWith("Page")) {
this.displayName = "Page " + key.slice(4); this.displayName = "Page " + key.slice(4);
@ -16,13 +17,21 @@ export default class TreeViewRequest {
} }
this.key = key; this.key = key;
this.children = children; 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 { 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) { public getChild(key: string) {
@ -30,16 +39,17 @@ export default class TreeViewRequest {
} }
public addChild(key: string) { public addChild(key: string) {
let child = new TreeViewRequest(key, []) this.expand = true;
let child = new TreeViewRequest(key, [], true)
this.children.push(child); this.children.push(child);
return child; return child;
} }
public clearChildren() {
this.children = [];
}
public removeChild(key: string) { public removeChild(key: string) {
this.children = this.children.filter(child => child.key !== key); 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 { invoke } from "@tauri-apps/api/core";
import Primitive from "./Primitive.svelte";
import TreeViewRequest from "./TreeViewRequest.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 { export default class TreeViewState {
private request: TreeViewRequest[];
private root: TreeViewRequest = $state(new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [])])); private activeRequest: Map<string, TreeViewRequest> = $state(new Map());
public view: Primitive | undefined = $state(); private activeEntries: Map<string, PrimitiveView[]> = $state(new Map());
active: boolean = $state(false);
file_id: string; file_id: string;
constructor(file_id: string, root: TreeViewRequest) { constructor(file_id: string, fState: FileViewState) {
console.log("Creating tree view state", file_id, root);
this.request = TreeViewRequest.fromPageCount(+fState.file.page_count);
this.activeRequest.set(this.request[0].key, this.request[0]);
this.file_id = file_id; this.file_id = file_id;
this.root = root;
} }
public loadTreeView() { public getEntryCount() {
this.setTreeViewRequest(this.getRoot()); let count = 0;
this.activeEntries.forEach((value, key) => {
count += value.length - 1;
});
return this.request.length + count;
} }
public getRoot(): TreeViewRequest { public getEntries(start: number, end: number): PrimitiveView[] {
return this.root.clone(); 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) { public toView(request: TreeViewRequest): PrimitiveView {
console.log("Loading tree view", treeViewRequest); return new PrimitiveView(
invoke<Primitive>("get_prim_tree_by_path", { 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, id: this.file_id,
path: treeViewRequest, paths: treeViewRequests,
}) });
.then((result) => { for (let i = 0; i < treeViewRequests.length; i++) {
this.view = new Primitive(result); let request = treeViewRequests[i];
this.root = treeViewRequest; if (request.expand) {
}) this.activeRequest.set(request.key, request);
.catch((err) => console.error(err)); } 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) { if (path.length == 0) {
console.error("Empty path"); console.error("Empty path");
return; return;
} }
console.log("Expanding tree", this.getRoot(), path); let root = this.activeRequest.get(path[0]);
let root = this.getRoot(); 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; let node = root;
for (let key of path.slice(1, path.length)) { for (let key of path.slice(1, path.length)) {
let _node: TreeViewRequest | undefined = node.getChild(key); let _node: TreeViewRequest | undefined = node.getChild(key);
if (_node) { if (_node) {
_node.setExpand(true);
node = _node; node = _node;
} else { } else {
node = node.addChild(key); 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) { if (path.length == 0) {
console.error("Empty path"); console.error("Empty path");
return; return;
} }
if (path.length == 1) { let root: TreeViewRequest | undefined = this.activeRequest.get(path[0]);
this.view = this.view?.withoutChildren(); if (!root) {
console.error("Root not found for path: " + path);
return; return;
} }
let root: TreeViewRequest = this.root;
let node: TreeViewRequest | undefined = 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); node = node?.getChild(key);
} }
if (node) { if (node) {
node.removeChild(path[path.length - 1]); node.setExpand(false);
this.setTreeViewRequest(root); await this.updateTreeViewRequest([root]);
} }
} }
} }

View File

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