Printing. An app that wanted to print had nothing in ltk to ask the user where and how, nor to reach a printer. `ltk::print` (`src/print/`) is a dialog built as `FileChooser` is: the app holds a `PrintDialog`, puts its `view()` in its own and hands its `PrintDialogMsg`s to `update()`, which returns a `PrintEvent`; `wrap` makes the app's message of the dialog's, so the printer list reaches it through the app's `ChannelSender`. It offers the destination, the printers CUPS knows with their state and "Save as PDF", the number of copies, all pages or a range such as `1-3, 5, 8-`, the paper (A4, Letter, Legal, A3, A5), the orientation, the margins, the scale, two-sided printing, colour and background graphics, the copies, sides and colour only for a printer. `PrintOptions` holds what was chosen and survives between dialogs, `parse_pages` and `format_pages` read and write the ranges. On a desktop the dialog is a card with ltk's `dialog()`, the preview on the left and the settings on the right; `compact()` makes it a whole page for a phone. `.printer( name )` and `.save_as_pdf()` choose where it starts. The preview. With `.preview( true )` the dialog asks the app for one with `PrintEvent::Preview( options )` whenever a setting that moves the pages changes, and shows what the app hands back to `PrintDialogMsg::previewed`, a `Preview` of some pages as RGBA with the document's total, one page at a time with buttons to move between them, greyed when colour is off. While one is being rendered a new request waits and is sent only if the layout changed meanwhile, so turning the scale quickly does not stack renders; until the first arrives the sheet's box shows "Preparing the preview…", kept to the sheet's width so it does not push the settings aside. CUPS. `print/cups.rs` loads `libcups.so.2` with `dlopen` at first use, so ltk neither links against it nor needs its headers, and a system without CUPS only loses its printers. It uses the destinations API, which also lists the network's driverless printers that have no queue: `printers()` with the default first, `print( printer, title, options, pdf )` sending the PDF as one job with `copies`, `media` as a PWG name, `sides` and `print-color-mode`, `jobs()`, `cancel()` and `set_default()`, which writes the user's `lpoptions`. `ltk::print::send`, `list_printers`, `jobs`, `cancel_job` and `set_default` run them on a thread and answer through the app's sender; `ltk::print::blocking` exposes them as they are, for code that has its own thread, such as a portal backend. `print/none.rs` stands in on Windows, Android and the browser, where only saving as PDF is offered. The portal. An app in a Flatpak cannot reach the CUPS socket. When `/.flatpak-info` exists, the dialog shows nothing of its own: it calls `org.freedesktop.portal.Print.PreparePrint` with the options as GTK's print settings and page setup, and the portal's own dialog answers; `Destination::Portal` then carries its token, and `send` passes the PDF to `Print` through a memfd. The Response signal is subscribed to on the request's path before the call, so a quick answer is not missed. `print/gtk.rs` translates `PrintOptions` to and from GTK's keys (`n-copies`, `page-ranges` counted from zero, `duplex`, `use-color`, `scale` in percent, the paper by its PWG name and size in millimetres), which a portal backend uses for the other direction. zbus 4, the one accesskit_unix already brings, joins the `wayland` feature for it. The dialog's strings are in ltk's seven locales. `file_chooser::view::icon` is shared with the dialog. Windows. The `windows` feature runs `ltk::run` in a Win32 window on Windows 10 1703 or later, with Windows's own frame, drawn through ANGLE's EGL and OpenGL ES when `libEGL.dll` and `libGLESv2.dll` sit beside the executable and in software through GDI otherwise (`src/platform/windows/`). It sizes the window by `App::window_size_hint` at the display's DPI and follows per-monitor DPI changes at fractional scales and the accessibility text size; it takes the mouse, the wheel, touch through `WM_POINTER`, and keys with Windows's repeat, dead keys and AltGr; it has the system clipboard, the cursor's shape, full screen, and moves and resizes begun from the app's own chrome, and keeps drawing while the window is moved or resized. Themes, fonts, keyboard layouts and MIME globs come from `ltk\` beside the executable, which `ltk::resource_dir` returns there by default; the font fallback chain and the monospace face are Windows's own, the system face stays the embedded Sora, and the file chooser offers each drive's root. A Win32 message loop cannot wait on calloop, so the runtime's timers and channels go through the queue the web backend already used: `build.rs` sets `cfg( queued_sources )` for both, and `AppData::dispatch_sources` moves from the web backend to `event_loop/sources.rs`. `Canvas::present_to` presents a GPU frame stretched over a window that a fractional scale left a pixel off it, and `EglContext::new_windows` and `create_native_surface` give ANGLE's display and an `HWND`'s surface. `App::save_state` is saved into `%LOCALAPPDATA%\<app_id>\` every 30 seconds, on exit and when the user's session ends, and restored as on Wayland: the part of the Wayland session runtime that does not speak the protocol, deciding why the app starts and handing it its bytes, is now `session_state::bootstrap`, which both backends call, with the file modes and the check for a running instance done each platform's way. `RunError::Window` reports a window Windows refused. `make windows-deps` and `make windows-check` typecheck the backend from Debian, compiling std from `rust-src`. `docs/windows.md` describes building, what ships beside the executable and what the backend does. ltk::dirs. `home`, `data_home`, `config_home`, `cache_home` and `state_home`: XDG's on Linux, with their usual fallbacks under the home folder, and on Windows `USERPROFILE`, `APPDATA` for data and settings and `LOCALAPPDATA` for the cache and state, an `XDG_*` variable that is set winning everywhere. An app finds its files there instead of reading `HOME` and the XDG variables itself, which on Windows are unset; ltk's own lookups of themes, keyboard layouts, MIME globs, the file chooser's folders and the session state now go through it, and `state_dir` takes the base it is given, its tests with it. Docs. The links to `WidgetId` in the crate's, `types`' and `App`'s documentation name the struct, since the `const fn` of the same name made them ambiguous to rustdoc. `README.md` names the other backends and `CHANGELOG.md` records the Windows backend and `ltk::dirs`. Not done. The print dialog has no CHANGELOG entry nor a page in `docs/widgets.md`. Under the portal there is no preview, since the portal's dialog gets the document only at the end. The desktop card's settings column does not scroll, so with a list open in a low window it may not fit. Tests cover the page ranges and the translation to GTK's keys, not the dialog, CUPS or the portal. On Windows there are no overlays, secondary surfaces or subsurfaces, no preedit for the input method and no accessibility.
638 lines
19 KiB
Rust
638 lines
19 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
//! A print dialog with a preview of the pages, and the system's printers through CUPS. The app renders the
|
|
//! document to PDF itself: the dialog asks it for a preview whenever the layout changes, and says where to
|
|
//! print and how once the user accepts.
|
|
//!
|
|
//! ```rust,no_run
|
|
//! # use ltk::{ ChannelSender, Element };
|
|
//! # use ltk::print::{ Destination, PrintDialog, PrintDialogMsg, PrintEvent };
|
|
//! # #[ derive( Clone ) ] enum Msg { Print( PrintDialogMsg ) }
|
|
//! # struct App { print: Option<PrintDialog<Msg>>, sender: ChannelSender<Msg> }
|
|
//! # impl App {
|
|
//! fn open( &mut self )
|
|
//! {
|
|
//! self.print = Some( PrintDialog::new( &self.sender, Msg::Print, "Report" ).preview( true ) );
|
|
//! }
|
|
//!
|
|
//! fn update( &mut self, msg: Msg )
|
|
//! {
|
|
//! let Msg::Print( msg ) = msg;
|
|
//! let Some( dialog ) = &mut self.print else { return };
|
|
//! match dialog.update( msg )
|
|
//! {
|
|
//! Some( PrintEvent::Preview( options ) ) => { /* render, then send PrintDialogMsg::previewed( .. ) */ }
|
|
//! Some( PrintEvent::Print( job ) ) => { self.print = None; /* render job.options, then ltk::print::send or save */ }
|
|
//! Some( PrintEvent::Cancelled ) => self.print = None,
|
|
//! None => {}
|
|
//! }
|
|
//! }
|
|
//! # fn view( &self ) -> Element<Msg> { self.print.as_ref().unwrap().view() }
|
|
//! # }
|
|
//! ```
|
|
|
|
mod options;
|
|
mod view;
|
|
pub mod gtk;
|
|
#[ cfg( all( feature = "wayland", target_os = "linux" ) ) ]
|
|
mod portal;
|
|
#[ cfg( all( unix, not( target_os = "android" ), not( target_arch = "wasm32" ) ) ) ]
|
|
mod cups;
|
|
#[ cfg( not( all( unix, not( target_os = "android" ), not( target_arch = "wasm32" ) ) ) ) ]
|
|
#[ path = "none.rs" ]
|
|
mod cups;
|
|
|
|
pub use options::{ format_pages, parse_pages, Margins, Orientation, Paper, PrintOptions, Sides, PAPERS };
|
|
|
|
/// CUPS's calls themselves, for a program without ltk's loop; each blocks.
|
|
pub mod blocking
|
|
{
|
|
pub use super::cups::{ cancel, jobs, print, printers, set_default };
|
|
}
|
|
|
|
use std::sync::Arc;
|
|
|
|
use crate::app::ChannelSender;
|
|
use crate::types::WidgetId;
|
|
use crate::wallpaper::ImageData;
|
|
use crate::widget::Element;
|
|
|
|
/// The field with the pages to print.
|
|
pub const PAGES: WidgetId = WidgetId( "ltk.print.pages" );
|
|
/// How wide, in pixels, the pages of a preview are best rendered.
|
|
pub const PREVIEW_WIDTH: u32 = 480;
|
|
|
|
#[ derive( Clone, Copy, Debug, PartialEq, Eq ) ]
|
|
pub enum PrinterState
|
|
{
|
|
Idle,
|
|
Printing,
|
|
Stopped,
|
|
Unknown,
|
|
}
|
|
|
|
#[ derive( Clone, Debug, PartialEq ) ]
|
|
pub struct Printer
|
|
{
|
|
/// The queue's name, which CUPS knows it by.
|
|
pub name: String,
|
|
pub instance: Option<String>,
|
|
/// What to call it: its description, or else its name.
|
|
pub description: String,
|
|
pub location: String,
|
|
pub model: String,
|
|
pub state: PrinterState,
|
|
/// IPP's `printer-state-reasons`, empty when there are none.
|
|
pub reasons: String,
|
|
pub accepting: bool,
|
|
pub default: bool,
|
|
}
|
|
|
|
#[ derive( Clone, Copy, Debug, PartialEq, Eq ) ]
|
|
pub enum JobState
|
|
{
|
|
Pending,
|
|
Held,
|
|
Printing,
|
|
Stopped,
|
|
Cancelled,
|
|
Aborted,
|
|
Completed,
|
|
}
|
|
|
|
impl JobState
|
|
{
|
|
pub( crate ) fn from_ipp( state: i32 ) -> Self
|
|
{
|
|
match state
|
|
{
|
|
3 => JobState::Pending,
|
|
4 => JobState::Held,
|
|
5 => JobState::Printing,
|
|
6 => JobState::Stopped,
|
|
7 => JobState::Cancelled,
|
|
8 => JobState::Aborted,
|
|
_ => JobState::Completed,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[ derive( Clone, Debug, PartialEq ) ]
|
|
pub struct Job
|
|
{
|
|
pub id: i32,
|
|
pub printer: String,
|
|
pub title: String,
|
|
pub state: JobState,
|
|
pub size_kb: u32,
|
|
/// Seconds since the epoch.
|
|
pub created: i64,
|
|
}
|
|
|
|
/// What the print portal's dialog chose, which it prints with.
|
|
#[ derive( Clone, Debug, PartialEq, Eq ) ]
|
|
pub struct PortalToken( pub( crate ) u32 );
|
|
|
|
#[ derive( Clone, Debug, PartialEq ) ]
|
|
pub enum Destination
|
|
{
|
|
Printer( Printer ),
|
|
Pdf,
|
|
/// The desktop prints for an app in a sandbox, where the user chose in its own dialog.
|
|
Portal( PortalToken ),
|
|
}
|
|
|
|
/// What to print, where and how, once the user accepted.
|
|
#[ derive( Clone, Debug, PartialEq ) ]
|
|
pub struct PrintJob
|
|
{
|
|
pub destination: Destination,
|
|
pub options: PrintOptions,
|
|
pub title: String,
|
|
}
|
|
|
|
#[ derive( Clone, Debug ) ]
|
|
pub struct PreviewPage
|
|
{
|
|
/// In the document, from 0.
|
|
pub index: usize,
|
|
pub image: ImageData,
|
|
}
|
|
|
|
/// The pages chosen, rendered with the options the dialog asked for.
|
|
#[ derive( Clone, Debug ) ]
|
|
pub struct Preview
|
|
{
|
|
/// The pages of the whole document.
|
|
pub total: usize,
|
|
pub pages: Vec<PreviewPage>,
|
|
}
|
|
|
|
/// What the app has to do for the dialog.
|
|
#[ derive( Clone, Debug, PartialEq ) ]
|
|
pub enum PrintEvent
|
|
{
|
|
/// Render a preview with these options and answer with [`PrintDialogMsg::previewed`].
|
|
Preview( PrintOptions ),
|
|
Print( PrintJob ),
|
|
Cancelled,
|
|
}
|
|
|
|
/// A setting whose choices the dialog shows under it.
|
|
#[ derive( Clone, Copy, Debug, PartialEq, Eq ) ]
|
|
pub enum Section
|
|
{
|
|
Destination,
|
|
Paper,
|
|
Orientation,
|
|
Margins,
|
|
Sides,
|
|
}
|
|
|
|
#[ derive( Clone, Debug ) ]
|
|
pub enum PrintDialogMsg
|
|
{
|
|
Start,
|
|
Printers( Result<Vec<Printer>, String> ),
|
|
/// The portal's dialog closed: the options and token, `None` when cancelled.
|
|
Prepared( Result<Option<( PrintOptions, u32 )>, String> ),
|
|
Previewed( Result<Preview, String> ),
|
|
Toggle( Section ),
|
|
/// A printer by its place in the list, or `None` for a PDF.
|
|
Destination( Option<usize> ),
|
|
Copies( i32 ),
|
|
AllPages,
|
|
SomePages,
|
|
Pages( String ),
|
|
Paper( usize ),
|
|
Orientation( Orientation ),
|
|
Margins( Margins ),
|
|
Scale( i32 ),
|
|
Color,
|
|
Sides( Sides ),
|
|
Backgrounds,
|
|
Page( i32 ),
|
|
Print,
|
|
Cancel,
|
|
}
|
|
|
|
impl PrintDialogMsg
|
|
{
|
|
/// The app's answer to [`PrintEvent::Preview`].
|
|
pub fn previewed( result: Result<Preview, String> ) -> Self
|
|
{
|
|
PrintDialogMsg::Previewed( result )
|
|
}
|
|
}
|
|
|
|
const SCALE_STEP: i32 = 10;
|
|
|
|
pub struct PrintDialog<Msg>
|
|
{
|
|
pub( crate ) title: String,
|
|
pub( crate ) printers: Vec<Printer>,
|
|
pub( crate ) listing: bool,
|
|
pub( crate ) list_error: Option<String>,
|
|
/// A printer of `printers`, or `None` for a PDF.
|
|
pub( crate ) destination: Option<usize>,
|
|
pub( crate ) wanted: Option<String>,
|
|
pub( crate ) prefer_pdf: bool,
|
|
pub( crate ) options: PrintOptions,
|
|
pub( crate ) some_pages: bool,
|
|
pub( crate ) pages_text: String,
|
|
pub( crate ) pages_bad: bool,
|
|
pub( crate ) open: Option<Section>,
|
|
pub( crate ) previews: bool,
|
|
pub( crate ) preview: Option<Preview>,
|
|
/// The preview's images as shown, in grey when printing without colour.
|
|
pub( crate ) images: Vec<ImageData>,
|
|
pub( crate ) preview_error: Option<String>,
|
|
/// The options a preview is being rendered with.
|
|
pub( crate ) rendering: Option<PrintOptions>,
|
|
pub( crate ) shown: usize,
|
|
pub( crate ) compact: bool,
|
|
/// The portal shows the dialog instead.
|
|
pub( crate ) portal: bool,
|
|
focus: Option<WidgetId>,
|
|
sender: ChannelSender<Msg>,
|
|
pub( crate ) wrap: Arc<dyn Fn( PrintDialogMsg ) -> Msg + Send + Sync>,
|
|
}
|
|
|
|
impl<Msg: Clone + Send + 'static> PrintDialog<Msg>
|
|
{
|
|
/// A dialog for printing the document called `title`. `wrap` makes the app's message of the dialog's own.
|
|
pub fn new( sender: &ChannelSender<Msg>, wrap: impl Fn( PrintDialogMsg ) -> Msg + Send + Sync + 'static, title: impl Into<String> ) -> Self
|
|
{
|
|
let wrap: Arc<dyn Fn( PrintDialogMsg ) -> Msg + Send + Sync> = Arc::new( wrap );
|
|
let _ = sender.send( wrap( PrintDialogMsg::Start ) );
|
|
let portal = sandboxed();
|
|
if !portal
|
|
{
|
|
let reply = Arc::clone( &wrap );
|
|
list_printers( sender, move |result| reply( PrintDialogMsg::Printers( result ) ) );
|
|
}
|
|
Self
|
|
{
|
|
title: title.into(),
|
|
printers: Vec::new(),
|
|
listing: !portal,
|
|
list_error: None,
|
|
destination: None,
|
|
wanted: None,
|
|
prefer_pdf: false,
|
|
options: PrintOptions::default(),
|
|
some_pages: false,
|
|
pages_text: String::new(),
|
|
pages_bad: false,
|
|
open: None,
|
|
previews: false,
|
|
preview: None,
|
|
images: Vec::new(),
|
|
preview_error: None,
|
|
rendering: None,
|
|
shown: 0,
|
|
compact: false,
|
|
portal,
|
|
focus: None,
|
|
sender: sender.clone(),
|
|
wrap,
|
|
}
|
|
}
|
|
|
|
/// Ask the app for a preview of the pages, which it renders on [`PrintEvent::Preview`].
|
|
pub fn preview( mut self, on: bool ) -> Self
|
|
{
|
|
self.previews = on;
|
|
self
|
|
}
|
|
|
|
/// Start with these options, as the app kept them from a [`PrintJob`].
|
|
pub fn options( mut self, options: PrintOptions ) -> Self
|
|
{
|
|
self.some_pages = !options.pages.is_empty();
|
|
self.pages_text = format_pages( &options.pages );
|
|
self.options = options;
|
|
self
|
|
}
|
|
|
|
/// Choose this printer, by its queue's name, rather than the default one once the printers are known.
|
|
pub fn printer( mut self, name: impl Into<String> ) -> Self
|
|
{
|
|
self.wanted = Some( name.into() );
|
|
self
|
|
}
|
|
|
|
/// Start with saving to PDF chosen, even when there are printers.
|
|
pub fn save_as_pdf( mut self ) -> Self
|
|
{
|
|
self.prefer_pdf = true;
|
|
self
|
|
}
|
|
|
|
/// Laid out for a narrow screen, on the whole of it.
|
|
pub fn compact( mut self, on: bool ) -> Self
|
|
{
|
|
self.compact = on;
|
|
self
|
|
}
|
|
|
|
/// The field to focus, once after it changed; an app hands it on from [`crate::App::take_focus_request`].
|
|
pub fn take_focus_request( &mut self ) -> Option<WidgetId>
|
|
{
|
|
self.focus.take()
|
|
}
|
|
|
|
/// Nothing while the portal shows its own dialog.
|
|
pub fn view( &self ) -> Element<Msg>
|
|
{
|
|
if self.portal { return crate::spacer().into(); }
|
|
view::view( self )
|
|
}
|
|
|
|
/// Take one of the dialog's messages; `Some` when the app has something to do.
|
|
pub fn update( &mut self, msg: PrintDialogMsg ) -> Option<PrintEvent>
|
|
{
|
|
match msg
|
|
{
|
|
PrintDialogMsg::Start if self.portal =>
|
|
{
|
|
let ( title, options, wrap ) = ( self.title.clone(), self.options.clone(), Arc::clone( &self.wrap ) );
|
|
run( &self.sender, move || prepare( &title, &options ), move |result| wrap( PrintDialogMsg::Prepared( result ) ) );
|
|
}
|
|
PrintDialogMsg::Start => return self.request_preview(),
|
|
PrintDialogMsg::Printers( result ) => self.listed( result ),
|
|
PrintDialogMsg::Prepared( result ) => match result
|
|
{
|
|
Ok( Some( ( options, token ) ) ) =>
|
|
{
|
|
return Some( PrintEvent::Print( PrintJob { destination: Destination::Portal( PortalToken( token ) ), options, title: self.title.clone() } ) );
|
|
}
|
|
Ok( None ) => return Some( PrintEvent::Cancelled ),
|
|
Err( error ) =>
|
|
{
|
|
self.portal = false;
|
|
self.list_error = Some( error );
|
|
return self.request_preview();
|
|
}
|
|
},
|
|
PrintDialogMsg::Previewed( result ) => return self.previewed( result ),
|
|
PrintDialogMsg::Toggle( section ) => self.open = if self.open == Some( section ) { None } else { Some( section ) },
|
|
PrintDialogMsg::Destination( index ) =>
|
|
{
|
|
self.destination = index.filter( |i| *i < self.printers.len() );
|
|
self.open = None;
|
|
}
|
|
PrintDialogMsg::Copies( delta ) =>
|
|
{
|
|
self.options.copies = ( self.options.copies as i64 + delta as i64 ).clamp( 1, PrintOptions::MAX_COPIES as i64 ) as u32;
|
|
}
|
|
PrintDialogMsg::AllPages =>
|
|
{
|
|
self.some_pages = false;
|
|
self.pages_bad = false;
|
|
return self.set_pages( Vec::new() );
|
|
}
|
|
PrintDialogMsg::SomePages =>
|
|
{
|
|
self.some_pages = true;
|
|
self.focus = Some( PAGES );
|
|
if let Some( pages ) = parse_pages( &self.pages_text ) { return self.set_pages( pages ); }
|
|
}
|
|
PrintDialogMsg::Pages( text ) =>
|
|
{
|
|
self.pages_text = text;
|
|
match parse_pages( &self.pages_text )
|
|
{
|
|
Some( pages ) =>
|
|
{
|
|
self.pages_bad = false;
|
|
return self.set_pages( pages );
|
|
}
|
|
None => self.pages_bad = !self.pages_text.trim().is_empty(),
|
|
}
|
|
}
|
|
PrintDialogMsg::Paper( index ) =>
|
|
{
|
|
self.open = None;
|
|
if let Some( paper ) = PAPERS.get( index ) { return self.change( |o| o.paper = *paper ); }
|
|
}
|
|
PrintDialogMsg::Orientation( orientation ) =>
|
|
{
|
|
self.open = None;
|
|
return self.change( |o| o.orientation = orientation );
|
|
}
|
|
PrintDialogMsg::Margins( margins ) =>
|
|
{
|
|
self.open = None;
|
|
return self.change( |o| o.margins = margins );
|
|
}
|
|
PrintDialogMsg::Scale( steps ) =>
|
|
{
|
|
let percent = ( self.options.scale * 100.0 ).round() as i32 + steps * SCALE_STEP;
|
|
let scale = ( percent as f32 / 100.0 ).clamp( PrintOptions::MIN_SCALE, PrintOptions::MAX_SCALE );
|
|
return self.change( |o| o.scale = scale );
|
|
}
|
|
PrintDialogMsg::Color =>
|
|
{
|
|
self.options.color = !self.options.color;
|
|
self.refresh_images();
|
|
}
|
|
PrintDialogMsg::Sides( sides ) =>
|
|
{
|
|
self.open = None;
|
|
self.options.sides = sides;
|
|
}
|
|
PrintDialogMsg::Backgrounds => return self.change( |o| o.backgrounds = !o.backgrounds ),
|
|
PrintDialogMsg::Page( delta ) =>
|
|
{
|
|
let last = self.images.len().saturating_sub( 1 ) as i64;
|
|
self.shown = ( self.shown as i64 + delta as i64 ).clamp( 0, last ) as usize;
|
|
}
|
|
PrintDialogMsg::Print => return self.job().map( PrintEvent::Print ),
|
|
PrintDialogMsg::Cancel => return Some( PrintEvent::Cancelled ),
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Where it prints to now.
|
|
pub fn destination( &self ) -> Destination
|
|
{
|
|
match self.destination.and_then( |i| self.printers.get( i ) )
|
|
{
|
|
Some( printer ) => Destination::Printer( printer.clone() ),
|
|
None => Destination::Pdf,
|
|
}
|
|
}
|
|
|
|
pub( crate ) fn msg( &self, msg: PrintDialogMsg ) -> Msg
|
|
{
|
|
( self.wrap )( msg )
|
|
}
|
|
|
|
pub( crate ) fn can_print( &self ) -> bool
|
|
{
|
|
!( self.some_pages && ( self.pages_bad || parse_pages( &self.pages_text ).is_none() ) )
|
|
}
|
|
|
|
fn job( &self ) -> Option<PrintJob>
|
|
{
|
|
if !self.can_print() { return None; }
|
|
let mut options = self.options.clone();
|
|
if !self.some_pages { options.pages.clear(); }
|
|
Some( PrintJob { destination: self.destination(), options, title: self.title.clone() } )
|
|
}
|
|
|
|
fn listed( &mut self, result: Result<Vec<Printer>, String> )
|
|
{
|
|
self.listing = false;
|
|
match result
|
|
{
|
|
Ok( printers ) =>
|
|
{
|
|
let wanted = self.wanted.as_deref();
|
|
self.destination = if self.prefer_pdf
|
|
{
|
|
None
|
|
} else {
|
|
printers.iter().position( |p| Some( p.name.as_str() ) == wanted )
|
|
.or_else( || printers.iter().position( |p| p.default ) )
|
|
.or( ( !printers.is_empty() ).then_some( 0 ) )
|
|
};
|
|
self.printers = printers;
|
|
}
|
|
Err( error ) => self.list_error = Some( error ),
|
|
}
|
|
}
|
|
|
|
fn set_pages( &mut self, pages: Vec<( u32, u32 )> ) -> Option<PrintEvent>
|
|
{
|
|
self.change( |o| o.pages = pages )
|
|
}
|
|
|
|
fn change( &mut self, apply: impl FnOnce( &mut PrintOptions ) ) -> Option<PrintEvent>
|
|
{
|
|
let before = self.options.clone();
|
|
apply( &mut self.options );
|
|
if before.same_layout( &self.options ) { return None; }
|
|
self.request_preview()
|
|
}
|
|
|
|
fn request_preview( &mut self ) -> Option<PrintEvent>
|
|
{
|
|
if !self.previews || self.rendering.is_some() { return None; }
|
|
self.rendering = Some( self.options.clone() );
|
|
Some( PrintEvent::Preview( self.options.clone() ) )
|
|
}
|
|
|
|
fn previewed( &mut self, result: Result<Preview, String> ) -> Option<PrintEvent>
|
|
{
|
|
let rendered = self.rendering.take();
|
|
match result
|
|
{
|
|
Ok( preview ) =>
|
|
{
|
|
self.preview_error = None;
|
|
self.shown = self.shown.min( preview.pages.len().saturating_sub( 1 ) );
|
|
self.preview = Some( preview );
|
|
self.refresh_images();
|
|
}
|
|
Err( error ) => self.preview_error = Some( error ),
|
|
}
|
|
match rendered
|
|
{
|
|
Some( rendered ) if !rendered.same_layout( &self.options ) => self.request_preview(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn refresh_images( &mut self )
|
|
{
|
|
let color = self.options.color;
|
|
self.images = self.preview.iter()
|
|
.flat_map( |preview| preview.pages.iter() )
|
|
.map( |page| if color { page.image.clone() } else { grey( &page.image ) } )
|
|
.collect();
|
|
}
|
|
}
|
|
|
|
fn grey( ( rgba, width, height ): &ImageData ) -> ImageData
|
|
{
|
|
let mut out = rgba.as_ref().clone();
|
|
for px in out.chunks_exact_mut( 4 )
|
|
{
|
|
let y = ( 0.299 * px[ 0 ] as f32 + 0.587 * px[ 1 ] as f32 + 0.114 * px[ 2 ] as f32 ).round() as u8;
|
|
px[ 0 ] = y;
|
|
px[ 1 ] = y;
|
|
px[ 2 ] = y;
|
|
}
|
|
( Arc::new( out ), *width, *height )
|
|
}
|
|
|
|
fn run<M: Send + 'static, T: Send + 'static>( sender: &ChannelSender<M>, work: impl FnOnce() -> T + Send + 'static, done: impl FnOnce( T ) -> M + Send + 'static )
|
|
{
|
|
let sender = sender.clone();
|
|
#[ cfg( target_arch = "wasm32" ) ]
|
|
let _ = sender.send( done( work() ) );
|
|
#[ cfg( not( target_arch = "wasm32" ) ) ]
|
|
std::thread::spawn( move || { let _ = sender.send( done( work() ) ); } );
|
|
}
|
|
|
|
/// The system's printers, the default first; empty without CUPS's server, an error without CUPS.
|
|
pub fn list_printers<M: Send + 'static>( sender: &ChannelSender<M>, done: impl FnOnce( Result<Vec<Printer>, String> ) -> M + Send + 'static )
|
|
{
|
|
run( sender, cups::printers, done );
|
|
}
|
|
|
|
/// Print `pdf` at `destination` with `options`; done once the printer's queue, or the desktop, took it.
|
|
pub fn send<M: Send + 'static>( destination: &Destination, title: &str, options: &PrintOptions, pdf: Vec<u8>, sender: &ChannelSender<M>, done: impl FnOnce( Result<(), String> ) -> M + Send + 'static )
|
|
{
|
|
let ( destination, title, options ) = ( destination.clone(), title.to_string(), options.clone() );
|
|
run( sender, move || match destination
|
|
{
|
|
Destination::Printer( printer ) => cups::print( &printer, &title, &options, &pdf ).map( |_| () ),
|
|
Destination::Portal( token ) => portal_print( token.0, &title, &pdf ),
|
|
Destination::Pdf => Err( rust_i18n::t!( "print.failed" ).to_string() ),
|
|
}, done );
|
|
}
|
|
|
|
#[ cfg( all( feature = "wayland", target_os = "linux" ) ) ]
|
|
use portal::{ prepare, print as portal_print, sandboxed };
|
|
|
|
#[ cfg( not( all( feature = "wayland", target_os = "linux" ) ) ) ]
|
|
fn sandboxed() -> bool
|
|
{
|
|
false
|
|
}
|
|
|
|
#[ cfg( not( all( feature = "wayland", target_os = "linux" ) ) ) ]
|
|
fn prepare( _title: &str, _options: &PrintOptions ) -> Result<Option<( PrintOptions, u32 )>, String>
|
|
{
|
|
Err( rust_i18n::t!( "print.no_cups" ).to_string() )
|
|
}
|
|
|
|
#[ cfg( not( all( feature = "wayland", target_os = "linux" ) ) ) ]
|
|
fn portal_print( _token: u32, _title: &str, _pdf: &[ u8 ] ) -> Result<(), String>
|
|
{
|
|
Err( rust_i18n::t!( "print.no_cups" ).to_string() )
|
|
}
|
|
|
|
/// The user's jobs not yet finished, on every printer or on one of them.
|
|
pub fn jobs<M: Send + 'static>( printer: Option<String>, sender: &ChannelSender<M>, done: impl FnOnce( Result<Vec<Job>, String> ) -> M + Send + 'static )
|
|
{
|
|
run( sender, move || cups::jobs( printer.as_deref() ), done );
|
|
}
|
|
|
|
pub fn cancel_job<M: Send + 'static>( printer: &str, job: i32, sender: &ChannelSender<M>, done: impl FnOnce( Result<(), String> ) -> M + Send + 'static )
|
|
{
|
|
let printer = printer.to_string();
|
|
run( sender, move || cups::cancel( &printer, job ), done );
|
|
}
|
|
|
|
/// Make `printer` the user's default.
|
|
pub fn set_default<M: Send + 'static>( printer: &Printer, sender: &ChannelSender<M>, done: impl FnOnce( Result<(), String> ) -> M + Send + 'static )
|
|
{
|
|
let ( name, instance ) = ( printer.name.clone(), printer.instance.clone() );
|
|
run( sender, move || cups::set_default( &name, instance.as_deref() ), done );
|
|
}
|