Compare commits
43 Commits
2ff15bbce0
...
master
Author | SHA1 | Date | |
---|---|---|---|
a00eade3bb | |||
76512404e1 | |||
02c0f24cd4 | |||
881da90290 | |||
d63eb2b9d1 | |||
b7a1a6c07e | |||
9d8713f622 | |||
636aa14be3 | |||
59e1852f62 | |||
2f7e9853aa | |||
02120b3263 | |||
a7ea7bd6d7 | |||
ac261f0761 | |||
4053fa70f4 | |||
b5239e14b6 | |||
c8795588d8 | |||
7110054836 | |||
f7b9036063 | |||
c6c07e788b | |||
d9d3438997 | |||
eef1f2caee | |||
f6a47eb8b7 | |||
72e19cd5a2 | |||
79ddd2680b | |||
b2441ec2dc | |||
77f32e55cf | |||
3f6fa16bdb | |||
3e373914c7 | |||
1e6b9fc337 | |||
78bc8b1e64 | |||
9240db298d | |||
1a7f7498cd | |||
75e1da0097 | |||
0a0fc61522 | |||
bb3e547660 | |||
e9b2263276 | |||
5659707132 | |||
b98ca6c9f6 | |||
1db3c212ba | |||
c6d8eb4a59 | |||
21ea774a00 | |||
2c524625c1 | |||
99e03d21a2 |
1214
Cargo.lock
generated
1214
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,13 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
poppler-rs = "0.22"
|
||||
cairo-rs = "0.18.3"
|
||||
cairo-rs = { version = "0.18.3", features = ["png"] }
|
||||
glib-macros = "0.18.3"
|
||||
gio = "0.18.3"
|
||||
glib = "0.18.3"
|
||||
clap = { version = "4.4.6", features = ["derive"] }
|
||||
gtk = { version = "0.7.3", package = "gtk4", features = ["v4_8"] }
|
||||
async-channel = "2.1.0"
|
||||
pdfium-render = "0.8.15"
|
||||
anyhow = "1.0.75"
|
||||
log = "0.4.20"
|
||||
env_logger = "0.10.1"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
|
14
de.frajul.music-reader.gschema.xml
Normal file
14
de.frajul.music-reader.gschema.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- GTK settings schema / configuration. Deploy this e.g. to $HOME/.local/share/glib-2.0/schemas -->
|
||||
<schemalist>
|
||||
<schema id="de.frajul.music-reader" path="/de/frajul/music-reader/">
|
||||
<key name="fullscreen" type="b">
|
||||
<default>false</default>
|
||||
<summary>Whether the application is in fullscreen mode</summary>
|
||||
</key>
|
||||
<key name="color-mode" type="s">
|
||||
<default>'Normal'</default>
|
||||
<summary>The color mode used to render the pdf pages</summary>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
62
flake.lock
generated
62
flake.lock
generated
@@ -1,15 +1,33 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694081375,
|
||||
"narHash": "sha256-vzJXOUnmkMCm3xw8yfPP5m8kypQ3BhAIRe4RRCWpzy8=",
|
||||
"lastModified": 1736429655,
|
||||
"narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "3f976d822b7b37fc6fb8e6f157c2dd05e7e94e89",
|
||||
"rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -21,12 +39,10 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1695806987,
|
||||
"narHash": "sha256-fX5kGs66NZIxCMcpAGIpxuftajHL8Hil1vjHmjjl118=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f3dab3509afca932f3f4fd0908957709bb1c1f57",
|
||||
"type": "github"
|
||||
"lastModified": 0,
|
||||
"narHash": "sha256-DjkQPnkAfd7eB522PwnkGhOMuT9QVCZspDpJJYyOj60=",
|
||||
"path": "/nix/store/g3jyakqb3ipnr6gz5rw10fb17ckr2z00-source",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
@@ -35,11 +51,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1695806987,
|
||||
"narHash": "sha256-fX5kGs66NZIxCMcpAGIpxuftajHL8Hil1vjHmjjl118=",
|
||||
"lastModified": 1736241350,
|
||||
"narHash": "sha256-CHd7yhaDigUuJyDeX0SADbTM9FXfiWaeNyY34FL1wQU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f3dab3509afca932f3f4fd0908957709bb1c1f57",
|
||||
"rev": "8c9fd3e564728e90829ee7dbac6edc972971cd0f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -51,9 +67,9 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"utils": "utils"
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
@@ -70,24 +86,6 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
109
flake.nix
109
flake.nix
@@ -1,43 +1,84 @@
|
||||
{
|
||||
inputs = {
|
||||
naersk.url = "github:nix-community/naersk/master";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
utils.url = "github:numtide/flake-utils";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
naersk.url = "github:nix-community/naersk/master";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, utils, naersk }:
|
||||
utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
naersk-lib = pkgs.callPackage naersk { };
|
||||
nativeBuildInputs = with pkgs; [
|
||||
gtk4
|
||||
cairo
|
||||
glib
|
||||
pkg-config
|
||||
poppler
|
||||
wrapGAppsHook
|
||||
];
|
||||
in {
|
||||
defaultPackage = naersk-lib.buildPackage {
|
||||
src = ./.;
|
||||
inherit nativeBuildInputs;
|
||||
};
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
naersk,
|
||||
}:
|
||||
let
|
||||
packageOutputs = flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
naersk-lib = pkgs.callPackage naersk { };
|
||||
|
||||
devShell = with pkgs;
|
||||
mkShell {
|
||||
# Needed at compile time (on build system)
|
||||
nativeBuildInputs = with pkgs; [
|
||||
gtk4
|
||||
cairo
|
||||
glib
|
||||
pkg-config
|
||||
poppler
|
||||
wrapGAppsHook
|
||||
];
|
||||
# Needed at runtime (on run system)
|
||||
buildInputs = with pkgs; [ ];
|
||||
in
|
||||
rec {
|
||||
defaultPackage = naersk-lib.buildPackage {
|
||||
src = ./.;
|
||||
inherit buildInputs;
|
||||
inherit nativeBuildInputs;
|
||||
buildInputs = [
|
||||
cargo
|
||||
rustc
|
||||
rustfmt
|
||||
rust-analyzer
|
||||
pre-commit
|
||||
rustPackages.clippy
|
||||
cargo-outdated
|
||||
cargo-audit
|
||||
];
|
||||
RUST_SRC_PATH = rustPlatform.rustLibSrc;
|
||||
|
||||
postInstall = ''
|
||||
mkdir -p $out/share/applications
|
||||
cp ${./music-reader.desktop} $out/share/applications/music-reader.desktop
|
||||
|
||||
mkdir -p $out/share/icons
|
||||
cp ${./music-reader.png} $out/share/icons/music-reader.png
|
||||
|
||||
mkdir -p $out/share/glib-2.0/schemas
|
||||
cp ${./de.frajul.music-reader.gschema.xml} $out/share/glib-2.0/schemas/de.frajul.music-reader.gschema.xml
|
||||
glib-compile-schemas $out/share/glib-2.0/schemas/
|
||||
'';
|
||||
};
|
||||
});
|
||||
devShell =
|
||||
with pkgs;
|
||||
mkShell {
|
||||
buildInputs = [
|
||||
cargo
|
||||
rustc
|
||||
rustfmt
|
||||
pre-commit
|
||||
rustPackages.clippy
|
||||
|
||||
gtk4
|
||||
cairo
|
||||
glib
|
||||
pkg-config
|
||||
poppler
|
||||
wrapGAppsHook
|
||||
];
|
||||
# Without inheriting nativeBuildinputs, cargo build will fail but that is good since we want to use only nix build
|
||||
# inherit nativeBuildInputs;
|
||||
|
||||
RUST_SRC_PATH = rustPlatform.rustLibSrc;
|
||||
};
|
||||
}
|
||||
);
|
||||
in
|
||||
packageOutputs
|
||||
// {
|
||||
hydraJobs = {
|
||||
x86_64-linux.music-reader = packageOutputs.defaultPackage.x86_64-linux;
|
||||
aarch64-linux.music-reader = packageOutputs.defaultPackage.aarch64-linux;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
9
music-reader.desktop
Normal file
9
music-reader.desktop
Normal file
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Name=Music Reader
|
||||
Icon=music-reader
|
||||
Categories=Viewer;Utility;
|
||||
Keywords=viewer;document;pdf;
|
||||
Exec=music-reader %F
|
||||
MimeType=application/pdf;
|
BIN
music-reader.png
Normal file
BIN
music-reader.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 488 KiB |
317
src/cache.rs
317
src/cache.rs
@@ -1,140 +1,319 @@
|
||||
use poppler::{Document, Page};
|
||||
use crate::{color_mode::{self, ColorMode}, draw};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use glib::timeout_future;
|
||||
use gtk::{gdk::Texture, prelude::TextureExt};
|
||||
use log::{debug, error};
|
||||
use poppler::Document;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
cell::RefCell,
|
||||
collections::{BTreeMap, VecDeque},
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use async_channel::Sender;
|
||||
|
||||
type PageNumber = usize;
|
||||
pub type MyPageType = Page;
|
||||
pub type PageNumber = usize;
|
||||
pub type MyPageType = Texture;
|
||||
|
||||
pub struct PageCache {
|
||||
document: Document,
|
||||
max_num_stored_pages: usize,
|
||||
pages: BTreeMap<usize, Rc<MyPageType>>,
|
||||
last_requested_page_number: PageNumber,
|
||||
color_mode: ColorMode,
|
||||
}
|
||||
|
||||
impl PageCache {
|
||||
pub fn new(document: Document, max_num_stored_pages: usize) -> Self {
|
||||
pub fn new(document: Document, max_num_stored_pages: usize, color_mode : ColorMode) -> Self {
|
||||
PageCache {
|
||||
document,
|
||||
max_num_stored_pages,
|
||||
pages: BTreeMap::new(),
|
||||
last_requested_page_number: 0,
|
||||
color_mode,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_page(&self, page_number: usize) -> Option<Rc<MyPageType>> {
|
||||
pub fn get_page(&mut self, page_number: usize) -> Option<Rc<MyPageType>> {
|
||||
self.last_requested_page_number = page_number;
|
||||
self.pages.get(&page_number).map(Rc::clone)
|
||||
}
|
||||
|
||||
pub fn cache_pages(&mut self, page_numbers: Vec<usize>) {
|
||||
println!("Caching pages {:?}", page_numbers);
|
||||
for page_number in page_numbers {
|
||||
if self.pages.contains_key(&page_number) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(page) = self.document.page(page_number as i32) {
|
||||
self.pages.insert(page_number, Rc::new(page));
|
||||
|
||||
if self.pages.len() > self.max_num_stored_pages && self.pages.len() > 2 {
|
||||
let _result = self.remove_most_distant_page(page_number);
|
||||
}
|
||||
pub fn get_page_or_cache(&mut self, page_number: usize) -> Result<Rc<MyPageType>> {
|
||||
if let Some(page) = self.get_page(page_number) {
|
||||
Ok(page)
|
||||
} else {
|
||||
let _ = self.cache_page(page_number, 100);
|
||||
if let Some(page) = self.get_page(page_number) {
|
||||
Ok(page)
|
||||
} else {
|
||||
bail!("Failed caching and retrieving page {}", page_number);
|
||||
}
|
||||
}
|
||||
println!("done caching");
|
||||
}
|
||||
|
||||
fn remove_most_distant_page(&mut self, current_page_number: usize) -> Result<(), ()> {
|
||||
let (min_cached_page_number, min_cached_page) = self.pages.pop_first().ok_or(())?;
|
||||
let (max_cached_page_number, max_cached_page) = self.pages.pop_last().ok_or(())?;
|
||||
pub fn cache_page(&mut self, page_number: PageNumber, height: i32) -> Result<Option<CacheResponse>> {
|
||||
debug!("Caching page {}", page_number);
|
||||
if page_number.abs_diff(self.last_requested_page_number)
|
||||
> self.max_num_stored_pages.div_ceil(2)
|
||||
{
|
||||
bail!("Page too far from reader, aborting caching call");
|
||||
}
|
||||
if let Some(page) = self.pages.get(&page_number) {
|
||||
if page.height() >= height {
|
||||
debug!("Page already in cache");
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
if page_number >= self.document.n_pages() as usize {
|
||||
bail!("Requested page {} has too high number and is not in the document", page_number);
|
||||
}
|
||||
|
||||
if current_page_number.abs_diff(min_cached_page_number)
|
||||
> current_page_number.abs_diff(max_cached_page_number)
|
||||
let begin_of_cashing = Instant::now();
|
||||
let mut response = None;
|
||||
|
||||
println!("Getting page form poppler: {}", page_number);
|
||||
if let Some(page) = self.document.page(page_number as i32) {
|
||||
let pages = vec![Rc::new(page)];
|
||||
let texture = draw::draw_pages_to_texture(&pages, height, &self.color_mode);
|
||||
let page = Rc::new(texture);
|
||||
|
||||
// Overwrite page with lower resolution if exists
|
||||
let previous_page = self.pages.insert(page_number, Rc::clone(&page));
|
||||
let page_resolution_upgraded = previous_page.is_some();
|
||||
if page_resolution_upgraded {
|
||||
response = Some(CacheResponse::PageResolutionUpgraded { page_number, page });
|
||||
}
|
||||
|
||||
if self.pages.len() > self.max_num_stored_pages && self.pages.len() > 2 {
|
||||
let _result = self.remove_most_distant_page();
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
"done caching of page {} in {}ms",
|
||||
page_number,
|
||||
begin_of_cashing.elapsed().as_millis()
|
||||
);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn remove_most_distant_page(&mut self) -> anyhow::Result<()> {
|
||||
let (min_cached_page_number, min_cached_page) = self
|
||||
.pages
|
||||
.pop_first()
|
||||
.ok_or(anyhow!("The cache is empty, cannot remove first page"))?;
|
||||
let (max_cached_page_number, max_cached_page) = self
|
||||
.pages
|
||||
.pop_last()
|
||||
.ok_or(anyhow!("The cache is empty, cannot remove last page"))?;
|
||||
|
||||
if self
|
||||
.last_requested_page_number
|
||||
.abs_diff(min_cached_page_number)
|
||||
> self
|
||||
.last_requested_page_number
|
||||
.abs_diff(max_cached_page_number)
|
||||
{
|
||||
self.pages.insert(max_cached_page_number, max_cached_page);
|
||||
debug!(
|
||||
"Removed page {} from cache to keep size low...",
|
||||
min_cached_page_number
|
||||
);
|
||||
} else {
|
||||
self.pages.insert(min_cached_page_number, min_cached_page);
|
||||
debug!(
|
||||
"Removed page {} from cache to keep size low...",
|
||||
max_cached_page_number
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_command(&mut self, command: CacheCommand) -> Option<CacheResponse> {
|
||||
println!("Processing command: {:?}...", command);
|
||||
fn process_command(&mut self, command: CacheCommand) -> Result<Option<CacheResponse>> {
|
||||
debug!("Processing command: {:?}...", command);
|
||||
match command {
|
||||
CacheCommand::CachePages { pages } => {
|
||||
self.cache_pages(pages);
|
||||
None
|
||||
}
|
||||
CacheCommand::GetCurrentTwoPages { page_left_number } => {
|
||||
if let Some(page_left) = self.get_page(page_left_number) {
|
||||
if let Some(page_right) = self.get_page(page_left_number + 1) {
|
||||
Some(CacheResponse::TwoPagesRetrieved {
|
||||
CacheCommand::Cache(command) => Ok(self.cache_page(command.page, command.height)?),
|
||||
CacheCommand::Retrieve(command) => match command {
|
||||
RetrievePagesCommand::GetCurrentTwoPages { page_left_number } => {
|
||||
let page_left = self.get_page_or_cache(page_left_number)?;
|
||||
if let Ok(page_right) = self.get_page_or_cache(page_left_number + 1) {
|
||||
Ok(Some(CacheResponse::TwoPagesRetrieved {
|
||||
page_number_left: page_left_number,
|
||||
page_left,
|
||||
page_number_right: page_left_number + 1,
|
||||
page_right,
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
Some(CacheResponse::SinglePageRetrieved { page: page_left })
|
||||
Ok(Some(CacheResponse::SinglePageRetrieved { page_number: page_left_number, page: page_left }))
|
||||
}
|
||||
} else {
|
||||
// TODO: if page left was not empty, this could be because page turning was too quick.
|
||||
// In this case, just not rendering the current page is okay, but when no more render requests are available, one would want to wait for the caching
|
||||
None
|
||||
}
|
||||
}
|
||||
RetrievePagesCommand::GetCurrentPage { page_number } => {
|
||||
let page = self.get_page_or_cache(page_number)?;
|
||||
Ok(Some(CacheResponse::SinglePageRetrieved { page_number, page }))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CacheCommand {
|
||||
CachePages { pages: Vec<PageNumber> },
|
||||
Cache(CachePageCommand),
|
||||
Retrieve(RetrievePagesCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CachePageCommand {
|
||||
page: PageNumber,
|
||||
height: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CacheKillSignal;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RetrievePagesCommand {
|
||||
GetCurrentTwoPages { page_left_number: PageNumber },
|
||||
GetCurrentPage { page_number: PageNumber },
|
||||
}
|
||||
|
||||
pub enum CacheResponse {
|
||||
DocumentLoaded {
|
||||
num_pages: usize,
|
||||
},
|
||||
SinglePageRetrieved {
|
||||
page_number: PageNumber,
|
||||
page: Rc<MyPageType>,
|
||||
},
|
||||
TwoPagesRetrieved {
|
||||
page_number_left: PageNumber,
|
||||
page_left: Rc<MyPageType>,
|
||||
page_number_right : PageNumber,
|
||||
page_right: Rc<MyPageType>,
|
||||
},
|
||||
PageResolutionUpgraded {
|
||||
page_number: PageNumber,
|
||||
page: Rc<MyPageType>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn spawn_async_cache<F>(file: impl AsRef<Path>, receiver: F) -> Sender<CacheCommand>
|
||||
pub struct SyncCacheCommandChannel {
|
||||
retrieve_commands: Vec<RetrievePagesCommand>,
|
||||
cache_commands: VecDeque<CachePageCommand>,
|
||||
priority_cache_commands: Vec<CachePageCommand>,
|
||||
kill_signals : Vec<CacheKillSignal>
|
||||
}
|
||||
|
||||
pub struct SyncCacheCommandSender {
|
||||
channel: Rc<RefCell<SyncCacheCommandChannel>>,
|
||||
}
|
||||
|
||||
pub struct SyncCacheCommandReceiver {
|
||||
cache_closed : bool,
|
||||
channel: Rc<RefCell<SyncCacheCommandChannel>>,
|
||||
}
|
||||
|
||||
impl SyncCacheCommandChannel {
|
||||
pub fn open() -> (SyncCacheCommandSender, SyncCacheCommandReceiver) {
|
||||
let channel = SyncCacheCommandChannel {
|
||||
retrieve_commands: Vec::new(),
|
||||
cache_commands: VecDeque::new(),
|
||||
priority_cache_commands: Vec::new(),
|
||||
kill_signals: Vec::new(),
|
||||
};
|
||||
let channel = Rc::new(RefCell::new(channel));
|
||||
|
||||
let sender = SyncCacheCommandSender {
|
||||
channel: Rc::clone(&channel),
|
||||
};
|
||||
let receiver = SyncCacheCommandReceiver { cache_closed: false, channel };
|
||||
(sender, receiver)
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncCacheCommandSender {
|
||||
pub fn _is_channel_open(&self) -> bool {
|
||||
Rc::strong_count(&self.channel) > 1
|
||||
}
|
||||
|
||||
pub fn send_retrieve_command(&self, command: RetrievePagesCommand) {
|
||||
// Make newest message the most important
|
||||
self.channel.borrow_mut().retrieve_commands.push(command);
|
||||
}
|
||||
|
||||
pub fn send_priority_cache_commands(&self, pages: &[PageNumber], height: i32) {
|
||||
for &page in pages {
|
||||
// Make message in front the most important
|
||||
self.channel
|
||||
.borrow_mut()
|
||||
.priority_cache_commands
|
||||
.push(CachePageCommand { page, height });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_cache_commands(&self, pages: &[PageNumber], height: i32) {
|
||||
for &page in pages {
|
||||
// Make message in front the most important
|
||||
// TODO: the low res cach will never be actually done
|
||||
self.channel
|
||||
.borrow_mut()
|
||||
.cache_commands
|
||||
.push_front(CachePageCommand { page, height: 100 }); // Cache with lower resolution
|
||||
self.channel
|
||||
.borrow_mut()
|
||||
.cache_commands
|
||||
.push_back(CachePageCommand { page, height });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_kill_signal(&self) {
|
||||
self.channel.borrow_mut().kill_signals.push(CacheKillSignal);
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncCacheCommandReceiver {
|
||||
pub fn is_channel_open(&self) -> bool {
|
||||
Rc::strong_count(&self.channel) > 1
|
||||
}
|
||||
|
||||
pub fn receive_most_important_command(&self) -> Option<CacheCommand> {
|
||||
let mut channel = self.channel.borrow_mut();
|
||||
if let Some(command) = channel.kill_signals.pop() {
|
||||
// self.cache_closed = true;
|
||||
return None;
|
||||
}
|
||||
if let Some(command) = channel.priority_cache_commands.pop() {
|
||||
return Some(CacheCommand::Cache(command));
|
||||
} else if let Some(command) = channel.retrieve_commands.pop() {
|
||||
return Some(CacheCommand::Retrieve(command));
|
||||
} else if let Some(command) = channel.cache_commands.pop_front() {
|
||||
return Some(CacheCommand::Cache(command));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_sync_cache<F>(document: Document, color_mode:ColorMode,receiver: F) -> SyncCacheCommandSender
|
||||
where
|
||||
F: Fn(CacheResponse) + 'static,
|
||||
{
|
||||
let (command_sender, command_receiver) = async_channel::unbounded();
|
||||
let (command_sender, command_receiver) = SyncCacheCommandChannel::open();
|
||||
|
||||
let path: PathBuf = file.as_ref().to_path_buf();
|
||||
let mut cache = PageCache::new(document, 30, color_mode);
|
||||
|
||||
// Besides the name, it is not in another thread
|
||||
glib::spawn_future_local(async move {
|
||||
println!("async loading of document:...");
|
||||
while command_receiver.is_channel_open() && !command_receiver.cache_closed {
|
||||
// Add delay to tell gtk to give rendering priority
|
||||
timeout_future(Duration::from_millis(1)).await;
|
||||
|
||||
let uri = format!("file://{}", path.to_str().unwrap());
|
||||
let document = poppler::Document::from_file(&uri, None).unwrap();
|
||||
let num_pages = document.n_pages() as usize;
|
||||
receiver(CacheResponse::DocumentLoaded { num_pages });
|
||||
|
||||
let mut cache = PageCache::new(document, 10);
|
||||
|
||||
while let Ok(command) = command_receiver.recv().await {
|
||||
// if !command_receiver.is_empty() {
|
||||
// // ignore command if more up to date ones are available
|
||||
// continue;
|
||||
// }
|
||||
if let Some(response) = cache.process_command(command).await {
|
||||
// response_sender.send_blocking(response).unwrap();
|
||||
println!("Command processed, activating receiver....");
|
||||
receiver(response);
|
||||
if let Some(command) = command_receiver.receive_most_important_command() {
|
||||
if let Some(response) = cache.process_command(command).unwrap_or_else(|e| {
|
||||
error!("Error processing command: {}", e);
|
||||
None
|
||||
}) {
|
||||
// response_sender.send_blocking(response).unwrap();
|
||||
debug!("Command processed, activating receiver....");
|
||||
receiver(response);
|
||||
debug!("receiver done");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
146
src/color_mode.rs
Normal file
146
src/color_mode.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use core::fmt;
|
||||
|
||||
use cairo::ImageSurface;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(Clone, EnumString, Debug)]
|
||||
pub enum ColorMode {
|
||||
Normal,
|
||||
Dark,
|
||||
Sepia,
|
||||
}
|
||||
|
||||
impl fmt::Display for ColorMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ColorMode {
|
||||
fn default() -> Self {
|
||||
ColorMode::Normal
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorMode {
|
||||
fn transform_color(&self, r: u8, g: u8, b: u8) -> (u8, u8, u8) {
|
||||
// l of black is 0.13 (approx 0x22)
|
||||
match self {
|
||||
ColorMode::Normal => (r, g, b),
|
||||
ColorMode::Dark => {
|
||||
let (mut h, mut s, mut l) = rgb_to_hsl(r, g, b);
|
||||
l = (1.0 - l) * (1.0 - 0.13) + 0.13;
|
||||
hsl_to_rgb(h, s, l)
|
||||
}
|
||||
ColorMode::Sepia => {
|
||||
let (mut h, mut s, mut l) = rgb_to_hsl(r, g, b);
|
||||
h = h + 30.0;
|
||||
if h < 0.0 {
|
||||
h += 360.0;
|
||||
}
|
||||
s += 0.2;
|
||||
if s > 1.0 {
|
||||
s = 1.0;
|
||||
}
|
||||
|
||||
l = l * (1.0 - 0.13);
|
||||
hsl_to_rgb(h, s, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply color mode transformation to an RGB24 surface.
|
||||
pub fn apply_to_rgb24_surface(&self, surface: &mut ImageSurface) {
|
||||
let width = surface.width();
|
||||
let height = surface.height();
|
||||
let stride = surface.stride();
|
||||
|
||||
let mut data = surface.data().expect("Failed to get source surface data");
|
||||
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let offset = (y * stride + x * 4) as usize; // Rgb24: 3 bytes per pixel (R, G, B)
|
||||
|
||||
// Read source pixel
|
||||
let r = data[offset + 2];
|
||||
let g = data[offset + 1];
|
||||
let b = data[offset];
|
||||
|
||||
// Apply color transformation
|
||||
let (tr, tg, tb) = self.transform_color(r, g, b);
|
||||
|
||||
// Write transformed pixel to destination surface
|
||||
data[offset + 2] = tr;
|
||||
data[offset + 1] = tg;
|
||||
data[offset] = tb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// h from 0 to 360
|
||||
// s from 0 to 1
|
||||
// l from 0 to 1
|
||||
fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f64, f64, f64) {
|
||||
let r = r as f64 / 255.0;
|
||||
let g = g as f64 / 255.0;
|
||||
let b = b as f64 / 255.0;
|
||||
|
||||
let max = r.max(g).max(b);
|
||||
let min = r.min(g).min(b);
|
||||
let delta = max - min;
|
||||
|
||||
// Calculate Lightness
|
||||
let l = (max + min) / 2.0;
|
||||
|
||||
// Calculate Saturation
|
||||
let s = if delta == 0.0 {
|
||||
0.0
|
||||
} else if l < 0.5 {
|
||||
delta / (max + min)
|
||||
} else {
|
||||
delta / (2.0 - max - min)
|
||||
};
|
||||
|
||||
// Calculate Hue
|
||||
let h = if delta == 0.0 {
|
||||
0.0
|
||||
} else if max == r {
|
||||
60.0 * (((g - b) / delta) % 6.0)
|
||||
} else if max == g {
|
||||
60.0 * (((b - r) / delta) + 2.0)
|
||||
} else {
|
||||
60.0 * (((r - g) / delta) + 4.0)
|
||||
};
|
||||
|
||||
let h = if h < 0.0 { h + 360.0 } else { h };
|
||||
|
||||
(h, s, l)
|
||||
}
|
||||
|
||||
fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (u8, u8, u8) {
|
||||
let c = (1.0 - (2.0 * l - 1.0).abs()) * s; // Chroma
|
||||
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
|
||||
let m = l - c / 2.0;
|
||||
|
||||
let (r, g, b) = if (0.0..60.0).contains(&h) {
|
||||
(c, x, 0.0)
|
||||
} else if (60.0..120.0).contains(&h) {
|
||||
(x, c, 0.0)
|
||||
} else if (120.0..180.0).contains(&h) {
|
||||
(0.0, c, x)
|
||||
} else if (180.0..240.0).contains(&h) {
|
||||
(0.0, x, c)
|
||||
} else if (240.0..300.0).contains(&h) {
|
||||
(x, 0.0, c)
|
||||
} else {
|
||||
(c, 0.0, x)
|
||||
};
|
||||
|
||||
// Convert back to [0, 255]
|
||||
(
|
||||
((r + m) * 255.0).round() as u8,
|
||||
((g + m) * 255.0).round() as u8,
|
||||
((b + m) * 255.0).round() as u8,
|
||||
)
|
||||
}
|
189
src/draw.rs
189
src/draw.rs
@@ -1,140 +1,83 @@
|
||||
use cairo::Context;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::ui::Ui;
|
||||
use gtk::{prelude::*, DrawingArea};
|
||||
use cairo::{Context, ImageSurface, ImageSurfaceData};
|
||||
use glib::Bytes;
|
||||
use gtk::gdk::Texture;
|
||||
use log::debug;
|
||||
use poppler::Page;
|
||||
|
||||
pub fn draw(ui: &mut Ui, area: &DrawingArea, context: &Context) {
|
||||
println!("Draw");
|
||||
if ui.document_canvas.is_none() {
|
||||
return;
|
||||
}
|
||||
let document_canvas = ui.document_canvas.as_ref().unwrap();
|
||||
use crate::color_mode::ColorMode;
|
||||
|
||||
if document_canvas.num_pages.unwrap_or(0) > 1 {
|
||||
draw_two_pages(ui, area, context);
|
||||
} else {
|
||||
draw_single_page(ui, area, context);
|
||||
}
|
||||
pub fn draw_pages_to_texture(
|
||||
pages: &[Rc<Page>],
|
||||
area_height: i32,
|
||||
color_mode: &ColorMode,
|
||||
) -> Texture {
|
||||
let area_height = i32::max(100, area_height);
|
||||
let total_width_normalized: f64 = pages
|
||||
.iter()
|
||||
.map(|page| page.size())
|
||||
.map(|(w, h)| w / h)
|
||||
.sum();
|
||||
let area_width = (total_width_normalized * area_height as f64 + 0.5) as i32;
|
||||
|
||||
println!("Finished drawing");
|
||||
document_canvas.cache_surrounding_pages();
|
||||
let mut surface = ImageSurface::create(cairo::Format::Rgb24, area_width, area_height).unwrap();
|
||||
{
|
||||
let context = Context::new(&surface).unwrap();
|
||||
draw_pages(pages, &context, area_width, area_height);
|
||||
} // Assure the context gets dropped and the data of the surface can be accessed again
|
||||
|
||||
color_mode.apply_to_rgb24_surface(&mut surface);
|
||||
|
||||
let mut stream: Vec<u8> = Vec::new();
|
||||
surface.write_to_png(&mut stream).unwrap();
|
||||
Texture::from_bytes(&Bytes::from(&stream)).unwrap()
|
||||
}
|
||||
|
||||
fn draw_two_pages(ui: &Ui, area: &DrawingArea, context: &Context) {
|
||||
if ui.document_canvas.is_none() {
|
||||
fn draw_pages(pages: &[Rc<Page>], context: &Context, area_width: i32, area_height: i32) {
|
||||
if pages.is_empty() {
|
||||
return;
|
||||
}
|
||||
let document_canvas = ui.document_canvas.as_ref().unwrap();
|
||||
let area_width = area_width as f64;
|
||||
let area_height = area_height as f64;
|
||||
|
||||
let page_left = document_canvas.left_page.as_ref();
|
||||
let page_right = document_canvas.right_page.as_ref();
|
||||
|
||||
if page_left.is_none() || page_right.is_none() {
|
||||
// TODO: show error message
|
||||
return;
|
||||
}
|
||||
|
||||
let page_left = page_left.unwrap();
|
||||
let page_right = page_right.unwrap();
|
||||
|
||||
// Add white background
|
||||
// context.set_source_rgba(1.0, 1.0, 1.0, 1.0);
|
||||
// context.fill().unwrap();
|
||||
// context.paint().unwrap();
|
||||
|
||||
let (w_left, h_left) = page_left.size();
|
||||
let (w_right, h_right) = page_right.size();
|
||||
|
||||
let h_max = f64::max(h_left, h_right);
|
||||
// Make sure both pages are rendered with the same height
|
||||
let w_max = match h_left < h_right {
|
||||
true => w_left * h_right / h_left + w_right,
|
||||
false => w_left + w_right * h_left / h_right,
|
||||
};
|
||||
|
||||
let h_scale = area.height() as f64 / h_max;
|
||||
let w_scale = area.width() as f64 / w_max;
|
||||
let scale = f64::min(h_scale, w_scale);
|
||||
let h_page = h_max * scale;
|
||||
|
||||
let scale_left = h_page / h_left;
|
||||
let scale_right = h_page / h_right;
|
||||
// Total width if height of every page was 1
|
||||
let total_width_normalized: f64 = pages
|
||||
.iter()
|
||||
.map(|page| page.size())
|
||||
.map(|(w, h)| w / h)
|
||||
.sum();
|
||||
// let height_to_scale_to = f64::min(area_width / total_width_normalized, area_height);
|
||||
let height_to_scale_to = area_height;
|
||||
let total_width = total_width_normalized * height_to_scale_to;
|
||||
|
||||
context.set_source_rgba(1.0, 1.0, 1.0, 1.0);
|
||||
context.translate(
|
||||
(area_width - total_width) / 2.0,
|
||||
(area_height - height_to_scale_to) / 2.0,
|
||||
);
|
||||
context.save().unwrap();
|
||||
context.translate(
|
||||
area.width() as f64 / 2.0 - w_left * scale_left,
|
||||
area.height() as f64 / 2.0 - h_page / 2.0,
|
||||
);
|
||||
// Poppler sometimes crops white border, draw it manually
|
||||
context.rectangle(0.0, 0.0, w_left * scale_left, h_page);
|
||||
context.fill().unwrap();
|
||||
context.scale(scale_left, scale_left);
|
||||
page_left.render(context);
|
||||
|
||||
context.restore().unwrap();
|
||||
context.translate(
|
||||
area.width() as f64 / 2.0,
|
||||
area.height() as f64 / 2.0 - h_page / 2.0,
|
||||
);
|
||||
// Poppler sometimes crops white border, draw it manually
|
||||
context.rectangle(0.0, 0.0, w_right * scale_right, h_page);
|
||||
context.fill().unwrap();
|
||||
context.scale(scale_right, scale_right);
|
||||
page_right.render(context);
|
||||
for page in pages {
|
||||
let (page_width, page_height) = page.size();
|
||||
let scale = height_to_scale_to / page_height;
|
||||
let scaled_width = page_width * scale;
|
||||
|
||||
let r = ui.drawing_context.paint();
|
||||
match r {
|
||||
Err(v) => println!("Error painting PDF: {v:?}"),
|
||||
Ok(_v) => {}
|
||||
}
|
||||
|
||||
ui.drawing_context.show_page().unwrap();
|
||||
}
|
||||
|
||||
fn draw_single_page(ui: &Ui, area: &DrawingArea, context: &Context) {
|
||||
if ui.document_canvas.is_none() {
|
||||
return;
|
||||
}
|
||||
let document_canvas = ui.document_canvas.as_ref().unwrap();
|
||||
|
||||
if document_canvas.left_page.is_none() {
|
||||
// TODO: show error message
|
||||
return;
|
||||
}
|
||||
|
||||
let page = document_canvas.left_page.as_ref().unwrap();
|
||||
|
||||
let (w, h) = page.size();
|
||||
|
||||
let width_diff = area.width() as f64 / w;
|
||||
let height_diff = area.height() as f64 / h;
|
||||
if width_diff > height_diff {
|
||||
context.translate(
|
||||
(area.width() as f64 - w * height_diff) / 2.0,
|
||||
(area.height() as f64 - h * height_diff) / 2.0,
|
||||
debug!(
|
||||
"drawing with size: {}, {}",
|
||||
scaled_width, height_to_scale_to
|
||||
);
|
||||
context.scale(height_diff, height_diff);
|
||||
} else {
|
||||
context.translate(
|
||||
(area.width() as f64 - w * width_diff) / 2.0,
|
||||
(area.height() as f64 - h * width_diff) / 2.0,
|
||||
);
|
||||
context.scale(width_diff, width_diff);
|
||||
|
||||
// context.translate(total_width_of_rendered_pages, 0.0);
|
||||
// Poppler sometimes crops white border, draw it manually
|
||||
context.rectangle(0.0, 0.0, scaled_width, height_to_scale_to);
|
||||
context.fill().unwrap();
|
||||
|
||||
context.scale(scale, scale);
|
||||
page.render(context);
|
||||
|
||||
context.restore().unwrap();
|
||||
context.translate(scaled_width, 0.0);
|
||||
context.save().unwrap();
|
||||
}
|
||||
|
||||
// Poppler sometimes crops white border, draw it manually
|
||||
context.set_source_rgba(1.0, 1.0, 1.0, 1.0);
|
||||
context.rectangle(0.0, 0.0, w, h);
|
||||
context.fill().unwrap();
|
||||
|
||||
page.render(context);
|
||||
|
||||
let r = ui.drawing_context.paint();
|
||||
match r {
|
||||
Err(v) => println!("Error painting PDF: {v:?}"),
|
||||
Ok(_v) => {}
|
||||
}
|
||||
|
||||
ui.drawing_context.show_page().unwrap();
|
||||
}
|
||||
|
14
src/main.rs
14
src/main.rs
@@ -1,8 +1,11 @@
|
||||
mod cache;
|
||||
mod color_mode;
|
||||
mod draw;
|
||||
mod ui;
|
||||
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
use gio::Settings;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use std::cell::RefCell;
|
||||
@@ -19,20 +22,21 @@ struct Cli {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init();
|
||||
let cli = Cli::parse();
|
||||
println!("Parse args");
|
||||
let app = Application::builder().application_id(APP_ID).build();
|
||||
|
||||
app.connect_activate(move |app| {
|
||||
let ui = build_ui(app);
|
||||
let settings = Settings::new(APP_ID);
|
||||
let ui = build_ui(app, settings);
|
||||
if let Some(file) = cli.file.as_ref() {
|
||||
ui::load_document(file, Rc::clone(&ui));
|
||||
ui::load_document(file, Rc::clone(&ui), 0);
|
||||
}
|
||||
});
|
||||
|
||||
app.run_with_args(&[] as &[&str]);
|
||||
}
|
||||
|
||||
fn build_ui(app: &Application) -> Rc<RefCell<Ui>> {
|
||||
Ui::build(app)
|
||||
fn build_ui(app: &Application, settings: Settings) -> Rc<RefCell<Ui>> {
|
||||
Ui::build(app, settings)
|
||||
}
|
||||
|
436
src/ui.rs
436
src/ui.rs
@@ -1,50 +1,59 @@
|
||||
use std::{cell::RefCell, path::Path, rc::Rc};
|
||||
|
||||
use async_channel::Sender;
|
||||
use cairo::{Context, Format, ImageSurface};
|
||||
use gtk::{
|
||||
glib, Application, ApplicationWindow, Box, Button, DrawingArea, FileChooserAction,
|
||||
FileChooserDialog, HeaderBar, Label, Orientation, ResponseType,
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
str::FromStr,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use gio::Settings;
|
||||
use gtk::{
|
||||
glib, Application, ApplicationWindow, Box, Button, FileChooserAction, FileChooserDialog,
|
||||
HeaderBar, Label, Overlay, Picture, ResponseType, ToggleButton,
|
||||
};
|
||||
use log::debug;
|
||||
|
||||
use crate::{
|
||||
cache::{self, CacheCommand, MyPageType},
|
||||
draw,
|
||||
cache::{self, CacheResponse, PageNumber, SyncCacheCommandSender},
|
||||
color_mode::{self, ColorMode},
|
||||
};
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
|
||||
pub struct Ui {
|
||||
settings: Settings,
|
||||
window: ApplicationWindow,
|
||||
bottom_bar: gtk::Box,
|
||||
header_bar: gtk::HeaderBar,
|
||||
page_indicator: gtk::Label,
|
||||
drawing_area: gtk::DrawingArea,
|
||||
pub drawing_context: cairo::Context,
|
||||
pub color_mode: ColorMode,
|
||||
pub app_wrapper: Overlay,
|
||||
pub image_container: Box,
|
||||
pub image_left: Picture,
|
||||
pub image_right: Picture,
|
||||
pub document_canvas: Option<DocumentCanvas>,
|
||||
pub last_touch_time: Option<Instant>,
|
||||
}
|
||||
|
||||
pub struct DocumentCanvas {
|
||||
current_page_number: usize,
|
||||
pub document_path: PathBuf,
|
||||
pub current_page_number: usize,
|
||||
pub num_pages: Option<usize>,
|
||||
page_cache_sender: Sender<CacheCommand>,
|
||||
pub left_page: Option<Rc<MyPageType>>,
|
||||
pub right_page: Option<Rc<MyPageType>>,
|
||||
page_cache_sender: SyncCacheCommandSender,
|
||||
}
|
||||
|
||||
impl DocumentCanvas {
|
||||
pub fn new(page_cache_sender: Sender<CacheCommand>) -> Self {
|
||||
pub fn new(document_path: PathBuf, page_cache_sender: SyncCacheCommandSender) -> Self {
|
||||
DocumentCanvas {
|
||||
document_path,
|
||||
current_page_number: 0,
|
||||
num_pages: None,
|
||||
page_cache_sender,
|
||||
left_page: None,
|
||||
right_page: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn increase_page_number(&mut self) {
|
||||
if self.current_page_number >= self.num_pages.unwrap_or(0).saturating_sub(2) {
|
||||
if self.current_page_number >= self.num_pages.unwrap_or(0).saturating_sub(1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,44 +64,77 @@ impl DocumentCanvas {
|
||||
self.current_page_number = self.current_page_number.saturating_sub(1);
|
||||
}
|
||||
|
||||
pub fn cache_initial_pages(&self) {
|
||||
self.page_cache_sender
|
||||
.send_blocking(CacheCommand::CachePages {
|
||||
pages: vec![self.current_page_number, self.current_page_number + 1],
|
||||
})
|
||||
.unwrap();
|
||||
pub fn cache_initial_pages(&self, area_height: i32) {
|
||||
self.priority_cache_current_pages(area_height);
|
||||
}
|
||||
|
||||
pub fn cache_surrounding_pages(&self) {
|
||||
self.page_cache_sender
|
||||
.send_blocking(CacheCommand::CachePages {
|
||||
pages: vec![
|
||||
self.current_page_number.saturating_sub(2),
|
||||
self.current_page_number.saturating_sub(1),
|
||||
self.current_page_number,
|
||||
self.current_page_number + 1,
|
||||
self.current_page_number + 2,
|
||||
self.current_page_number + 3,
|
||||
],
|
||||
})
|
||||
.unwrap();
|
||||
pub fn priority_cache_current_pages(&self, area_height: i32) {
|
||||
self.page_cache_sender.send_priority_cache_commands(
|
||||
&[self.current_page_number, self.current_page_number + 1],
|
||||
area_height,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn purge_cache(&self) {
|
||||
self.page_cache_sender.send_kill_signal();
|
||||
}
|
||||
|
||||
pub fn cache_surrounding_pages(&self, area_height: i32) {
|
||||
self.page_cache_sender.send_cache_commands(
|
||||
&[
|
||||
self.current_page_number,
|
||||
self.current_page_number + 1,
|
||||
self.current_page_number.saturating_sub(1),
|
||||
self.current_page_number + 2,
|
||||
self.current_page_number.saturating_sub(2),
|
||||
self.current_page_number + 3,
|
||||
],
|
||||
area_height,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn request_to_draw_pages(&self) {
|
||||
self.page_cache_sender
|
||||
.send_blocking(CacheCommand::GetCurrentTwoPages {
|
||||
page_left_number: self.current_page_number,
|
||||
})
|
||||
.unwrap();
|
||||
if self.num_pages == Some(1) {
|
||||
self.page_cache_sender.send_retrieve_command(
|
||||
cache::RetrievePagesCommand::GetCurrentPage {
|
||||
page_number: self.current_page_number,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
self.page_cache_sender.send_retrieve_command(
|
||||
cache::RetrievePagesCommand::GetCurrentTwoPages {
|
||||
page_left_number: self.current_page_number,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_left_page(&self, page_number: PageNumber) -> bool {
|
||||
page_number == self.current_page_number
|
||||
}
|
||||
pub fn is_right_page(&self, page_number: PageNumber) -> bool {
|
||||
page_number == self.current_page_number + 1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_fullscreen(ui: &Ui) {
|
||||
match !ui.window.is_fullscreen() {
|
||||
pub fn set_fullscreen(ui: &Ui, fullscreen: bool) {
|
||||
match fullscreen {
|
||||
true => {
|
||||
ui.header_bar.hide();
|
||||
ui.bottom_bar.hide();
|
||||
ui.window.fullscreen();
|
||||
|
||||
let new_area_height = ui.image_container.height() + ui.header_bar.height();
|
||||
if ui.document_canvas.is_some() {
|
||||
ui.document_canvas
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.priority_cache_current_pages(new_area_height);
|
||||
ui.document_canvas
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.cache_surrounding_pages(new_area_height);
|
||||
}
|
||||
}
|
||||
false => {
|
||||
ui.header_bar.show();
|
||||
@@ -100,6 +142,11 @@ pub fn toggle_fullscreen(ui: &Ui) {
|
||||
ui.window.unfullscreen();
|
||||
}
|
||||
}
|
||||
ui.settings.set_boolean("fullscreen", fullscreen).unwrap();
|
||||
}
|
||||
|
||||
pub fn toggle_fullscreen(ui: &Ui) {
|
||||
set_fullscreen(ui, !ui.window.is_fullscreen());
|
||||
}
|
||||
|
||||
fn update_page_status(ui: &Ui) {
|
||||
@@ -137,22 +184,35 @@ fn process_right_click(ui: &mut Ui, _x: f64, _y: f64) {
|
||||
}
|
||||
|
||||
fn process_left_click(ui: &mut Ui, x: f64, y: f64) {
|
||||
if let Some(last_touch_time) = ui.last_touch_time {
|
||||
if last_touch_time.elapsed() < Duration::from_millis(100) {
|
||||
// Prevent accidental double touching
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ui.last_touch_time = Some(Instant::now());
|
||||
if ui.document_canvas.is_none() {
|
||||
return;
|
||||
}
|
||||
let edge_touch_area_size = f64::min(100.0, ui.app_wrapper.height() as f64 / 10.0);
|
||||
|
||||
let center = ui.drawing_area.width() / 2;
|
||||
if y < (ui.drawing_area.height() / 5) as f64 {
|
||||
let center = ui.app_wrapper.width() / 2;
|
||||
if y < edge_touch_area_size {
|
||||
if x > ui.app_wrapper.width() as f64 - edge_touch_area_size {
|
||||
ui.window.close();
|
||||
return;
|
||||
}
|
||||
toggle_fullscreen(ui);
|
||||
} else if x > center as f64 {
|
||||
if x < ui.drawing_area.width() as f64 * 0.75 {
|
||||
if x < ui.app_wrapper.width() as f64 * 0.75 {
|
||||
ui.document_canvas.as_mut().unwrap().increase_page_number();
|
||||
} else {
|
||||
ui.document_canvas.as_mut().unwrap().increase_page_number();
|
||||
ui.document_canvas.as_mut().unwrap().increase_page_number();
|
||||
}
|
||||
} else if x < center as f64 {
|
||||
if x > ui.drawing_area.width() as f64 * 0.25 {
|
||||
if x > ui.app_wrapper.width() as f64 * 0.25 {
|
||||
ui.document_canvas.as_mut().unwrap().decrease_page_number();
|
||||
} else {
|
||||
ui.document_canvas.as_mut().unwrap().decrease_page_number();
|
||||
@@ -162,43 +222,105 @@ fn process_left_click(ui: &mut Ui, x: f64, y: f64) {
|
||||
update_page_status(ui);
|
||||
}
|
||||
|
||||
fn create_drawing_context() -> Context {
|
||||
let surface = ImageSurface::create(Format::Rgb24, 0, 0).unwrap();
|
||||
Context::new(&surface).unwrap()
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
pub fn build(app: &Application) -> Rc<RefCell<Ui>> {
|
||||
println!("building ui");
|
||||
pub fn build(app: &Application, settings: Settings) -> Rc<RefCell<Ui>> {
|
||||
debug!("building ui");
|
||||
let open_file_button = Button::from_icon_name("document-open");
|
||||
let fullscreen_button = Button::from_icon_name("view-fullscreen");
|
||||
let normal_color_mode_button = ToggleButton::builder().label("Std").build();
|
||||
let dark_color_mode_button = ToggleButton::builder()
|
||||
.label("Dark")
|
||||
.group(&normal_color_mode_button)
|
||||
.build();
|
||||
let sepia_color_mode_button = ToggleButton::builder()
|
||||
.label("Sepia")
|
||||
.group(&normal_color_mode_button)
|
||||
.build();
|
||||
|
||||
let app_wrapper = Box::builder().orientation(Orientation::Vertical).build();
|
||||
let button_container = Box::builder()
|
||||
.spacing(10)
|
||||
.hexpand(true)
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.build();
|
||||
let color_mode_button_container = Box::builder()
|
||||
.spacing(0)
|
||||
.hexpand(true)
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.build();
|
||||
color_mode_button_container.append(&normal_color_mode_button);
|
||||
color_mode_button_container.append(&dark_color_mode_button);
|
||||
color_mode_button_container.append(&sepia_color_mode_button);
|
||||
button_container.append(&open_file_button);
|
||||
button_container.append(&fullscreen_button);
|
||||
button_container.append(&color_mode_button_container);
|
||||
|
||||
let image_container = Box::builder()
|
||||
.spacing(0)
|
||||
// .width_request(600)
|
||||
// .height_request(300)
|
||||
.vexpand(true)
|
||||
.hexpand(true)
|
||||
.halign(gtk::Align::Center)
|
||||
.build();
|
||||
let image_left = Picture::builder()
|
||||
// .width_request(300)
|
||||
// .height_request(300)
|
||||
.vexpand(true)
|
||||
// .hexpand(true)
|
||||
.build();
|
||||
let image_right = Picture::builder()
|
||||
// .width_request(300)
|
||||
// .height_request(300)
|
||||
.vexpand(true)
|
||||
// .hexpand(true)
|
||||
.build();
|
||||
image_container.append(&image_left);
|
||||
image_container.append(&image_right);
|
||||
|
||||
let app_wrapper = Overlay::builder()
|
||||
// .orientation(Orientation::Vertical)
|
||||
.hexpand(true)
|
||||
.vexpand(true)
|
||||
.child(&image_container)
|
||||
.build();
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.title("Music Reader")
|
||||
.child(&app_wrapper)
|
||||
.maximized(true)
|
||||
.width_request(600)
|
||||
.height_request(400)
|
||||
.build();
|
||||
window.present();
|
||||
|
||||
let color_mode = ColorMode::from_str(&settings.string("color-mode")).unwrap_or_default();
|
||||
match color_mode {
|
||||
ColorMode::Normal => normal_color_mode_button.set_active(true),
|
||||
ColorMode::Dark => dark_color_mode_button.set_active(true),
|
||||
ColorMode::Sepia => sepia_color_mode_button.set_active(true),
|
||||
};
|
||||
|
||||
let ui = Ui {
|
||||
settings,
|
||||
window,
|
||||
bottom_bar: Box::builder().hexpand_set(true).build(),
|
||||
app_wrapper,
|
||||
bottom_bar: Box::builder()
|
||||
.hexpand_set(true)
|
||||
.valign(gtk::Align::End)
|
||||
.build(),
|
||||
header_bar: HeaderBar::builder().build(),
|
||||
page_indicator: Label::builder().build(),
|
||||
drawing_area: DrawingArea::builder()
|
||||
.width_request(400)
|
||||
.height_request(300)
|
||||
.hexpand(true)
|
||||
.vexpand(true)
|
||||
.build(),
|
||||
drawing_context: create_drawing_context(),
|
||||
image_container,
|
||||
image_left,
|
||||
image_right,
|
||||
document_canvas: None,
|
||||
last_touch_time: None,
|
||||
color_mode,
|
||||
};
|
||||
let ui = Rc::new(RefCell::new(ui));
|
||||
|
||||
ui.borrow().header_bar.pack_start(&open_file_button);
|
||||
app_wrapper.prepend(&ui.borrow().drawing_area);
|
||||
app_wrapper.append(&ui.borrow().bottom_bar);
|
||||
ui.borrow().header_bar.pack_start(&button_container);
|
||||
ui.borrow().app_wrapper.add_overlay(&ui.borrow().bottom_bar);
|
||||
ui.borrow().bottom_bar.append(&ui.borrow().page_indicator);
|
||||
|
||||
let click_left = gtk::GestureClick::new();
|
||||
@@ -213,14 +335,8 @@ impl Ui {
|
||||
process_right_click(&mut ui.borrow_mut(), x, y);
|
||||
}));
|
||||
|
||||
ui.borrow().drawing_area.add_controller(click_left);
|
||||
ui.borrow().drawing_area.add_controller(click_right);
|
||||
|
||||
ui.borrow().drawing_area.set_draw_func(
|
||||
glib::clone!(@weak ui => move |area, context, _, _| {
|
||||
draw::draw(&mut ui.borrow_mut(), area, context);
|
||||
}),
|
||||
);
|
||||
ui.borrow().app_wrapper.add_controller(click_left);
|
||||
ui.borrow().app_wrapper.add_controller(click_right);
|
||||
|
||||
ui.borrow()
|
||||
.window
|
||||
@@ -231,12 +347,68 @@ impl Ui {
|
||||
choose_file(Rc::clone(&ui), &ui.borrow().window);
|
||||
}),
|
||||
);
|
||||
fullscreen_button.connect_clicked(
|
||||
glib::clone!(@strong ui => @default-panic, move |_button| {
|
||||
toggle_fullscreen(&ui.borrow());
|
||||
}),
|
||||
);
|
||||
normal_color_mode_button.connect_clicked(
|
||||
glib::clone!(@strong ui => @default-panic, move |_button| {
|
||||
switch_color_mode(Rc::clone(&ui), ColorMode::Normal);
|
||||
}),
|
||||
);
|
||||
dark_color_mode_button.connect_clicked(
|
||||
glib::clone!(@strong ui => @default-panic, move |_button| {
|
||||
switch_color_mode(Rc::clone(&ui), ColorMode::Dark);
|
||||
}),
|
||||
);
|
||||
sepia_color_mode_button.connect_clicked(
|
||||
glib::clone!(@strong ui => @default-panic, move |_button| {
|
||||
switch_color_mode(Rc::clone(&ui), ColorMode::Sepia);
|
||||
}),
|
||||
);
|
||||
|
||||
ui.borrow().window.present();
|
||||
|
||||
if ui.borrow().settings.boolean("fullscreen") {
|
||||
set_fullscreen(&ui.borrow(), true);
|
||||
}
|
||||
|
||||
ui
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_color_mode(ui: Rc<RefCell<Ui>>, color_mode: ColorMode) {
|
||||
ui.borrow()
|
||||
.settings
|
||||
.set_string("color-mode", &color_mode.to_string())
|
||||
.unwrap();
|
||||
|
||||
ui.borrow_mut().color_mode = color_mode;
|
||||
|
||||
if ui.borrow().document_canvas.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
ui.borrow().document_canvas.as_ref().unwrap().purge_cache();
|
||||
|
||||
let path = ui
|
||||
.borrow()
|
||||
.document_canvas
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.document_path
|
||||
.clone();
|
||||
let current_page_number = ui
|
||||
.borrow()
|
||||
.document_canvas
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.current_page_number;
|
||||
|
||||
load_document(path, Rc::clone(&ui), current_page_number);
|
||||
}
|
||||
|
||||
fn choose_file(ui: Rc<RefCell<Ui>>, window: &ApplicationWindow) {
|
||||
let filechooser = FileChooserDialog::builder()
|
||||
.title("Choose a PDF...")
|
||||
@@ -249,46 +421,102 @@ fn choose_file(ui: Rc<RefCell<Ui>>, window: &ApplicationWindow) {
|
||||
filechooser.connect_response(move |d, response| {
|
||||
if response == ResponseType::Accept {
|
||||
let path = d.file().unwrap().path().unwrap();
|
||||
load_document(path, Rc::clone(&ui));
|
||||
|
||||
if ui.borrow().document_canvas.is_some() {
|
||||
ui.borrow().document_canvas.as_ref().unwrap().purge_cache();
|
||||
}
|
||||
|
||||
load_document(path, Rc::clone(&ui), 0);
|
||||
}
|
||||
d.destroy();
|
||||
});
|
||||
filechooser.show()
|
||||
}
|
||||
|
||||
pub fn load_document(file: impl AsRef<Path>, ui: Rc<RefCell<Ui>>) {
|
||||
println!("Loading file...");
|
||||
pub fn load_document(file: impl AsRef<Path>, ui: Rc<RefCell<Ui>>, initial_page_number: usize) {
|
||||
debug!("Loading file...");
|
||||
// TODO: catch errors, maybe show error dialog
|
||||
let path: PathBuf = file.as_ref().to_path_buf();
|
||||
let uri = format!("file://{}", path.to_str().unwrap());
|
||||
let document = poppler::Document::from_file(&uri, None).unwrap();
|
||||
let num_pages = document.n_pages() as usize;
|
||||
let color_mode = ui.borrow().color_mode.clone();
|
||||
|
||||
let sender = cache::spawn_async_cache(
|
||||
file,
|
||||
clone!(@weak ui => move |cache_response| match cache_response {
|
||||
cache::CacheResponse::DocumentLoaded { num_pages } => {
|
||||
ui.borrow_mut().document_canvas.as_mut().unwrap().num_pages = Some(num_pages);
|
||||
update_page_status(&ui.borrow())
|
||||
}
|
||||
cache::CacheResponse::SinglePageRetrieved { page } => {
|
||||
ui.borrow_mut().document_canvas.as_mut().unwrap().left_page = Some(page);
|
||||
ui.borrow_mut().document_canvas.as_mut().unwrap().right_page = None;
|
||||
ui.borrow().drawing_area.queue_draw();
|
||||
}
|
||||
cache::CacheResponse::TwoPagesRetrieved {
|
||||
page_left,
|
||||
page_right,
|
||||
} => {
|
||||
ui.borrow_mut().document_canvas.as_mut().unwrap().left_page = Some(page_left);
|
||||
ui.borrow_mut().document_canvas.as_mut().unwrap().right_page = Some(page_right);
|
||||
ui.borrow().drawing_area.queue_draw();
|
||||
}
|
||||
}),
|
||||
let sender = cache::spawn_sync_cache(
|
||||
document,
|
||||
color_mode,
|
||||
clone!(@weak ui => move |cache_response| handle_cache_response(ui, cache_response)),
|
||||
);
|
||||
|
||||
println!("Spawned async cache");
|
||||
let mut document_canvas = DocumentCanvas::new(path, sender);
|
||||
document_canvas.num_pages = Some(num_pages);
|
||||
document_canvas.current_page_number = initial_page_number;
|
||||
document_canvas.cache_initial_pages(ui.borrow().image_container.height());
|
||||
|
||||
let document_canvas = DocumentCanvas::new(sender);
|
||||
document_canvas.cache_initial_pages();
|
||||
ui.borrow_mut().document_canvas = Some(document_canvas);
|
||||
|
||||
update_page_status(&ui.borrow());
|
||||
println!("finished loading document");
|
||||
debug!("finished loading document");
|
||||
}
|
||||
|
||||
fn handle_cache_response(ui: Rc<RefCell<Ui>>, cache_response: CacheResponse) {
|
||||
if let Some(canvas) = ui.borrow().document_canvas.as_ref() {
|
||||
let image_left = &ui.borrow().image_left;
|
||||
let image_right = &ui.borrow().image_right;
|
||||
let area_height = ui.borrow().image_container.height();
|
||||
|
||||
match cache_response {
|
||||
cache::CacheResponse::SinglePageRetrieved { page_number, page } => {
|
||||
if !canvas.is_left_page(page_number) {
|
||||
debug!("Retrieved page with non up-to-date page_number from cache, ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
image_left.set_paintable(Some(page.as_ref()));
|
||||
image_left.queue_draw();
|
||||
if canvas.num_pages.unwrap_or(0) > 1 {
|
||||
// Make image invisible but keep free space in layout
|
||||
image_right.set_opacity(0.0);
|
||||
} else {
|
||||
// Make image invisible and center left page in layout
|
||||
image_right.set_visible(false);
|
||||
}
|
||||
canvas.cache_surrounding_pages(area_height);
|
||||
}
|
||||
cache::CacheResponse::TwoPagesRetrieved {
|
||||
page_number_left,
|
||||
page_left,
|
||||
page_number_right,
|
||||
page_right,
|
||||
} => {
|
||||
if !canvas.is_left_page(page_number_left)
|
||||
|| !canvas.is_right_page(page_number_right)
|
||||
{
|
||||
debug!("Retrieved pages with non up-to-date page_number from cache, ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
image_left.set_paintable(Some(page_left.as_ref()));
|
||||
image_right.set_paintable(Some(page_right.as_ref()));
|
||||
image_right.set_visible(true);
|
||||
image_right.set_opacity(1.0);
|
||||
|
||||
image_left.queue_draw();
|
||||
image_right.queue_draw();
|
||||
|
||||
canvas.cache_surrounding_pages(area_height);
|
||||
}
|
||||
cache::CacheResponse::PageResolutionUpgraded { page_number, page } => {
|
||||
if canvas.is_left_page(page_number) {
|
||||
image_left.set_paintable(Some(page.as_ref()));
|
||||
|
||||
image_left.queue_draw();
|
||||
} else if canvas.is_right_page(page_number) {
|
||||
image_right.set_paintable(Some(page.as_ref()));
|
||||
|
||||
image_right.queue_draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user