Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion apps/pdf-viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ temporary directory and renders its first page. Passing a local PDF path opens
that document instead. The **Open PDF** command uses Avalonia's platform file
picker, and **Previous**/**Next** navigate rendered pages.

The left **Bookmarks** pane reads the PDF outline (table of contents) the same
way Acrobat Reader does: nested entries expand in place, and clicking a
bookmark seeks to that page. **View → Bookmarks** (Ctrl+B) shows or hides the
pane. Documents without an `/Outlines` dictionary show an empty-state message.

## Build on Windows x64

From the repository root:
Expand All @@ -40,12 +45,22 @@ Pop-Location
```

After building, the Windows UI Automation smoke test exercises startup sample
generation, page rendering, next/previous navigation and natural shutdown:
generation, page rendering, bookmark jumps, next/previous navigation and
natural shutdown:

```powershell
powershell.exe -NoProfile -STA -ExecutionPolicy Bypass `
-File ./apps/pdf-viewer/tests/test-bundle.ps1
```

To exercise Acrobat-style table-of-contents navigation on a real document:

```powershell
powershell.exe -NoProfile -STA -ExecutionPolicy Bypass `
-File ./apps/pdf-viewer/tests/test-bundle.ps1 `
-PdfPath 'C:\Users\mamoreau\Downloads\[MS-RDPBCGR].pdf' `
-TimeoutSeconds 90
```

The app is a portable Win32 GUI bundle, not an installer. File-association
metadata snippets are under `file-associations/`.
4 changes: 2 additions & 2 deletions apps/pdf-viewer/generated/.avalonia-viewmodel.owned.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 112 additions & 2 deletions apps/pdf-viewer/generated/generated_view_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,21 @@ impl MainViewModelSink {
pub fn set_search_match_label(&self, value: impl AsRef<str>) -> crate::Result<()> { self.0.set_string(12, value) }
pub fn set_can_go_previous_match(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(13, value) }
pub fn set_can_go_next_match(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(14, value) }
pub fn set_outline_visible(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(15, value) }
pub fn set_has_outline(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(16, value) }
pub fn set_outline_status(&self, value: impl AsRef<str>) -> crate::Result<()> { self.0.set_string(17, value) }
pub fn add_recent_files(&self, value: impl AsRef<str>) -> crate::Result<()> { self.0.add_string(1, value) }
pub fn insert_recent_files(&self, index: i32, value: impl AsRef<str>) -> crate::Result<()> { self.0.insert_string(1, index, value) }
pub fn replace_recent_files(&self, index: i32, value: impl AsRef<str>) -> crate::Result<()> { self.0.replace_string(1, index, value) }
pub fn add_outline(&self, value: impl OutlineItemViewModel) -> crate::Result<()> { self.0.add_model(2, OutlineItemViewModelDispatch { model: value }) }
pub fn insert_outline(&self, index: i32, value: impl OutlineItemViewModel) -> crate::Result<()> { self.0.insert_model(2, index, OutlineItemViewModelDispatch { model: value }) }
pub fn replace_outline(&self, index: i32, value: impl OutlineItemViewModel) -> crate::Result<()> { self.0.replace_model(2, index, OutlineItemViewModelDispatch { model: value }) }
pub fn remove_recent_files(&self, index: i32) -> crate::Result<()> { self.0.remove_string_at(1, index) }
pub fn move_recent_files(&self, from_index: i32, to_index: i32) -> crate::Result<()> { self.0.move_string_item(1, from_index, to_index) }
pub fn clear_recent_files(&self) -> crate::Result<()> { self.0.clear_string_collection(1) }
pub fn remove_outline(&self, index: i32) -> crate::Result<()> { self.0.remove_model_at(2, index) }
pub fn move_outline(&self, from_index: i32, to_index: i32) -> crate::Result<()> { self.0.move_model_item(2, from_index, to_index) }
pub fn clear_outline(&self) -> crate::Result<()> { self.0.clear_model_collection(2) }
pub fn set_open_file_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(1, enabled) }
pub fn set_previous_page_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(2, enabled) }
pub fn set_next_page_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(3, enabled) }
Expand Down Expand Up @@ -61,6 +70,9 @@ impl MainViewModelSink {
pub fn set_search_match_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(12, message) }
pub fn set_can_go_previous_match_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(13, message) }
pub fn set_can_go_next_match_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(14, message) }
pub fn set_outline_visible_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(15, message) }
pub fn set_has_outline_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(16, message) }
pub fn set_outline_status_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(17, message) }
/// Creates a worker-safe immutable update batch with a monotonic generation.
pub fn batch(&self, generation: i64) -> MainViewModelSinkBatch { MainViewModelSinkBatch(crate::view_model::ViewModelBatch::new(generation)) }
pub fn submit_batch(&self, batch: MainViewModelSinkBatch) -> crate::Result<crate::view_model::BatchCompletion> { self.0.submit_batch(batch.0) }
Expand Down Expand Up @@ -111,13 +123,29 @@ impl MainViewModelSinkBatch {
pub fn set_can_go_next_match(&mut self, value: bool) { self.0.push_boolean(3, 14, value); }
pub fn set_can_go_next_match_error(&mut self, message: impl AsRef<str>) { self.0.push_string(18, 14, 0, message); }
pub fn clear_can_go_next_match_error(&mut self) { self.0.push_clear_error(14); }
pub fn set_outline_visible(&mut self, value: bool) { self.0.push_boolean(3, 15, value); }
pub fn set_outline_visible_error(&mut self, message: impl AsRef<str>) { self.0.push_string(18, 15, 0, message); }
pub fn clear_outline_visible_error(&mut self) { self.0.push_clear_error(15); }
pub fn set_has_outline(&mut self, value: bool) { self.0.push_boolean(3, 16, value); }
pub fn set_has_outline_error(&mut self, message: impl AsRef<str>) { self.0.push_string(18, 16, 0, message); }
pub fn clear_has_outline_error(&mut self) { self.0.push_clear_error(16); }
pub fn set_outline_status(&mut self, value: impl AsRef<str>) { self.0.push_string(1, 17, 0, value); }
pub fn set_outline_status_error(&mut self, message: impl AsRef<str>) { self.0.push_string(18, 17, 0, message); }
pub fn clear_outline_status_error(&mut self) { self.0.push_clear_error(17); }
pub fn add_recent_files(&mut self, value: impl AsRef<str>) { self.0.push_string(7, 1, 0, value); }
pub fn insert_recent_files(&mut self, index: i32, value: impl AsRef<str>) { self.0.push_string(9, 1, index, value); }
pub fn replace_recent_files(&mut self, index: i32, value: impl AsRef<str>) { self.0.push_string(11, 1, index, value); }
pub fn replace_recent_files_snapshot<S: AsRef<str>>(&mut self, values: impl IntoIterator<Item = S>) { self.0.push_string_snapshot(1, values); }
pub fn remove_recent_files(&mut self, index: i32) { self.0.push_indices(13, 1, index, 0); }
pub fn move_recent_files(&mut self, from_index: i32, to_index: i32) { self.0.push_indices(14, 1, from_index, to_index); }
pub fn clear_recent_files(&mut self) { self.0.push_indices(19, 1, 0, 0); }
pub fn add_outline(&mut self, value: impl OutlineItemViewModel) { self.0.push_model(8, 2, 0, OutlineItemViewModelDispatch { model: value }); }
pub fn insert_outline(&mut self, index: i32, value: impl OutlineItemViewModel) { self.0.push_model(10, 2, index, OutlineItemViewModelDispatch { model: value }); }
pub fn replace_outline(&mut self, index: i32, value: impl OutlineItemViewModel) { self.0.push_model(12, 2, index, OutlineItemViewModelDispatch { model: value }); }
pub fn replace_outline_snapshot<M: OutlineItemViewModel>(&mut self, values: impl IntoIterator<Item = M>) { self.0.push_model_snapshot(2, values.into_iter().map(|value| OutlineItemViewModelDispatch { model: value })); }
pub fn remove_outline(&mut self, index: i32) { self.0.push_model_indices(13, 2, index, 0); }
pub fn move_outline(&mut self, from_index: i32, to_index: i32) { self.0.push_model_indices(14, 2, from_index, to_index); }
pub fn clear_outline(&mut self) { self.0.push_model_clear(2); }
pub fn set_open_file_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 1, enabled); }
pub fn set_previous_page_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 2, enabled); }
pub fn set_next_page_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 3, enabled); }
Expand All @@ -134,6 +162,7 @@ pub trait MainViewModel: Send + 'static {
fn attach(&mut self, sink: MainViewModelSink) -> crate::Result<()>;
fn detach(&mut self) -> crate::Result<()>;
fn set_search_text(&mut self, value: String) -> crate::Result<()>;
fn set_outline_visible(&mut self, value: bool) -> crate::Result<()>;
fn open_file(&mut self) -> crate::Result<()>;
fn previous_page(&mut self) -> crate::Result<()>;
fn next_page(&mut self) -> crate::Result<()>;
Expand All @@ -158,8 +187,11 @@ impl<T: MainViewModel> crate::view_model::DynamicViewModel for MainViewModelDisp
fn set_integer(&mut self, property_id: i32, _value: i64) -> crate::Result<()> {
Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
}
fn set_boolean(&mut self, property_id: i32, _value: bool) -> crate::Result<()> {
Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
fn set_boolean(&mut self, property_id: i32, value: bool) -> crate::Result<()> {
match property_id {
15 => self.model.set_outline_visible(value),
_ => Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }),
}
}
fn set_double(&mut self, property_id: i32, _value: f64) -> crate::Result<()> {
Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
Expand All @@ -185,3 +217,81 @@ impl<T: MainViewModel> crate::view_model::DynamicViewModel for MainViewModelDisp
}

pub fn mount_main_window(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model(1, MainViewModelDispatch { model }) }

#[derive(Clone, Debug)]
pub struct OutlineItemViewModelSink(crate::view_model::ViewModelSink);

impl OutlineItemViewModelSink {
pub fn set_title(&self, value: impl AsRef<str>) -> crate::Result<()> { self.0.set_string(1, value) }
pub fn set_page_label(&self, value: impl AsRef<str>) -> crate::Result<()> { self.0.set_string(2, value) }
pub fn set_has_children(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(3, value) }
pub fn add_children(&self, value: impl OutlineItemViewModel) -> crate::Result<()> { self.0.add_model(1, OutlineItemViewModelDispatch { model: value }) }
pub fn insert_children(&self, index: i32, value: impl OutlineItemViewModel) -> crate::Result<()> { self.0.insert_model(1, index, OutlineItemViewModelDispatch { model: value }) }
pub fn replace_children(&self, index: i32, value: impl OutlineItemViewModel) -> crate::Result<()> { self.0.replace_model(1, index, OutlineItemViewModelDispatch { model: value }) }
pub fn remove_children(&self, index: i32) -> crate::Result<()> { self.0.remove_model_at(1, index) }
pub fn move_children(&self, from_index: i32, to_index: i32) -> crate::Result<()> { self.0.move_model_item(1, from_index, to_index) }
pub fn clear_children(&self) -> crate::Result<()> { self.0.clear_model_collection(1) }
pub fn set_go_to_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(1, enabled) }
pub fn set_title_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(1, message) }
pub fn set_page_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(2, message) }
pub fn set_has_children_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(3, message) }
/// Creates a worker-safe immutable update batch with a monotonic generation.
pub fn batch(&self, generation: i64) -> OutlineItemViewModelSinkBatch { OutlineItemViewModelSinkBatch(crate::view_model::ViewModelBatch::new(generation)) }
pub fn submit_batch(&self, batch: OutlineItemViewModelSinkBatch) -> crate::Result<crate::view_model::BatchCompletion> { self.0.submit_batch(batch.0) }
}

pub struct OutlineItemViewModelSinkBatch(crate::view_model::ViewModelBatch);

impl OutlineItemViewModelSinkBatch {
pub fn set_title(&mut self, value: impl AsRef<str>) { self.0.push_string(1, 1, 0, value); }
pub fn set_title_error(&mut self, message: impl AsRef<str>) { self.0.push_string(18, 1, 0, message); }
pub fn clear_title_error(&mut self) { self.0.push_clear_error(1); }
pub fn set_page_label(&mut self, value: impl AsRef<str>) { self.0.push_string(1, 2, 0, value); }
pub fn set_page_label_error(&mut self, message: impl AsRef<str>) { self.0.push_string(18, 2, 0, message); }
pub fn clear_page_label_error(&mut self) { self.0.push_clear_error(2); }
pub fn set_has_children(&mut self, value: bool) { self.0.push_boolean(3, 3, value); }
pub fn set_has_children_error(&mut self, message: impl AsRef<str>) { self.0.push_string(18, 3, 0, message); }
pub fn clear_has_children_error(&mut self) { self.0.push_clear_error(3); }
pub fn add_children(&mut self, value: impl OutlineItemViewModel) { self.0.push_model(8, 1, 0, OutlineItemViewModelDispatch { model: value }); }
pub fn insert_children(&mut self, index: i32, value: impl OutlineItemViewModel) { self.0.push_model(10, 1, index, OutlineItemViewModelDispatch { model: value }); }
pub fn replace_children(&mut self, index: i32, value: impl OutlineItemViewModel) { self.0.push_model(12, 1, index, OutlineItemViewModelDispatch { model: value }); }
pub fn replace_children_snapshot<M: OutlineItemViewModel>(&mut self, values: impl IntoIterator<Item = M>) { self.0.push_model_snapshot(1, values.into_iter().map(|value| OutlineItemViewModelDispatch { model: value })); }
pub fn remove_children(&mut self, index: i32) { self.0.push_model_indices(13, 1, index, 0); }
pub fn move_children(&mut self, from_index: i32, to_index: i32) { self.0.push_model_indices(14, 1, from_index, to_index); }
pub fn clear_children(&mut self) { self.0.push_model_clear(1); }
pub fn set_go_to_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 1, enabled); }
}

pub trait OutlineItemViewModel: Send + 'static {
fn attach(&mut self, sink: OutlineItemViewModelSink) -> crate::Result<()>;
fn detach(&mut self) -> crate::Result<()>;
fn go_to(&mut self) -> crate::Result<()>;
}

struct OutlineItemViewModelDispatch<T: OutlineItemViewModel> { model: T }

impl<T: OutlineItemViewModel> crate::view_model::DynamicViewModel for OutlineItemViewModelDispatch<T> {
fn attach(&mut self, sink: crate::view_model::ViewModelSink) -> crate::Result<()> { self.model.attach(OutlineItemViewModelSink(sink)) }
fn detach(&mut self) -> crate::Result<()> { self.model.detach() }
fn set_string(&mut self, property_id: i32, _value: String) -> crate::Result<()> {
Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
}
fn set_integer(&mut self, property_id: i32, _value: i64) -> crate::Result<()> {
Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
}
fn set_boolean(&mut self, property_id: i32, _value: bool) -> crate::Result<()> {
Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
}
fn set_double(&mut self, property_id: i32, _value: f64) -> crate::Result<()> {
Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
}
fn execute(&mut self, command_id: i32, _parameter: Option<String>) -> crate::Result<()> {
match command_id {
1 => self.model.go_to(),
_ => Err(crate::Error::InvalidViewModelMember { kind: "command", id: command_id }),
}
}
fn begin_async(&mut self, command_id: i32, _parameter: Option<String>) -> crate::Result<()> {
Err(crate::Error::InvalidViewModelMember { kind: "command", id: command_id })
}
}
20 changes: 20 additions & 0 deletions apps/pdf-viewer/generated/view-model.contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ Schema version: `5`
| Property | 12 | `SearchMatchLabel` | `String` | Rust to managed |
| Property | 13 | `CanGoPreviousMatch` | `Boolean` | Rust to managed |
| Property | 14 | `CanGoNextMatch` | `Boolean` | Rust to managed |
| Property | 15 | `OutlineVisible` | `Boolean` | Rust and managed |
| Property | 16 | `HasOutline` | `Boolean` | Rust to managed |
| Property | 17 | `OutlineStatus` | `String` | Rust to managed |
| Collection | 1 | `RecentFiles` | `String` | Rust to managed |
| Collection | 2 | `Outline` | Model `OutlineItemViewModel` (tree root) | Rust to managed |
| Async command | 1 | `OpenFile` | None | Managed to Rust |
| Command | 2 | `PreviousPage` | None | Managed to Rust |
| Command | 3 | `NextPage` | None | Managed to Rust |
Expand All @@ -30,6 +34,10 @@ Schema version: `5`
| Command | 7 | `PreviousMatch` | None | Managed to Rust |
| Command | 8 | `NextMatch` | None | Managed to Rust |

### Tree `Outline`

Node model `OutlineItemViewModel`, children `Children`, header `Title`, has-children `HasChildren`.

### Recent files `RecentFiles`

Storage URIs published into collection `RecentFiles`, capacity 5, activated by `OpenRecentFileCommand` with the chosen URI as its command parameter.
Expand All @@ -46,6 +54,18 @@ Storage URIs published into collection `RecentFiles`, capacity 5, activated by `
| 6 | `Page` | Submenu | _Page | - | - | - |
| 7 |     `PreviousPage` | Command | _Previous page | `PreviousPageCommand` | `Alt+Left` | - |
| 8 |     `NextPage` | Command | _Next page | `NextPageCommand` | `Alt+Right` | - |
| 9 | `View` | Submenu | _View | - | - | - |
| 10 |     `Bookmarks` | Toggle | _Bookmarks | - | `Ctrl+B` | `OutlineVisible` |

## Model `OutlineItemViewModel` (`2`)

| Kind | ID | Name | Type | Direction |
| --- | ---: | --- | --- | --- |
| Property | 1 | `Title` | `String` | Rust to managed |
| Property | 2 | `PageLabel` | `String` | Rust to managed |
| Property | 3 | `HasChildren` | `Boolean` | Rust to managed |
| Collection | 1 | `Children` | Model `OutlineItemViewModel` (recursive children) | Rust to managed |
| Command | 1 | `GoTo` | None | Managed to Rust |

## Views

Expand Down
14 changes: 11 additions & 3 deletions apps/pdf-viewer/managed/Generated/.avalonia-viewmodel.owned.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading