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 };
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;
pub const PAGES: WidgetId = WidgetId( "ltk.print.pages" );
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
{
pub name: String,
pub instance: Option<String>,
pub description: String,
pub location: String,
pub model: String,
pub state: PrinterState,
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,
pub created: i64,
}
#[ derive( Clone, Debug, PartialEq, Eq ) ]
pub struct PortalToken( pub( crate ) u32 );
#[ derive( Clone, Debug, PartialEq ) ]
pub enum Destination
{
Printer( Printer ),
Pdf,
Portal( PortalToken ),
}
#[ derive( Clone, Debug, PartialEq ) ]
pub struct PrintJob
{
pub destination: Destination,
pub options: PrintOptions,
pub title: String,
}
#[ derive( Clone, Debug ) ]
pub struct PreviewPage
{
pub index: usize,
pub image: ImageData,
}
#[ derive( Clone, Debug ) ]
pub struct Preview
{
pub total: usize,
pub pages: Vec<PreviewPage>,
}
#[ derive( Clone, Debug, PartialEq ) ]
pub enum PrintEvent
{
Preview( PrintOptions ),
Print( PrintJob ),
Cancelled,
}
#[ 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> ),
Prepared( Result<Option<( PrintOptions, u32 )>, String> ),
Previewed( Result<Preview, String> ),
Toggle( Section ),
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
{
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>,
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>,
pub( crate ) images: Vec<ImageData>,
pub( crate ) preview_error: Option<String>,
pub( crate ) rendering: Option<PrintOptions>,
pub( crate ) shown: usize,
pub( crate ) compact: bool,
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>
{
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,
}
}
pub fn preview( mut self, on: bool ) -> Self
{
self.previews = on;
self
}
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
}
pub fn printer( mut self, name: impl Into<String> ) -> Self
{
self.wanted = Some( name.into() );
self
}
pub fn save_as_pdf( mut self ) -> Self
{
self.prefer_pdf = true;
self
}
pub fn compact( mut self, on: bool ) -> Self
{
self.compact = on;
self
}
pub fn take_focus_request( &mut self ) -> Option<WidgetId>
{
self.focus.take()
}
pub fn view( &self ) -> Element<Msg>
{
if self.portal { return crate::spacer().into(); }
view::view( self )
}
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
}
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() ) ); } );
}
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 );
}
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() )
}
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 );
}
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 );
}