use std::{cmp::min, f64::consts::PI, fmt::Write, ops::DerefMut, sync::Arc};
use cairo::Context;
use enum_map::{EnumMap, enum_map};
use itertools::Itertools;
use pango::{
AttrFloat, AttrFontDesc, AttrInt, AttrList, Attribute, FontDescription, FontMask, Layout,
SCALE, SCALE_SMALL, Underline, Weight,
};
use pangocairo::functions::show_layout;
use smallvec::{SmallVec, smallvec};
use crate::{
output::{
Details, Item,
drivers::cairo::{px_to_xr, xr_to_pt},
pivot::{
Axis2, Coord2, PivotTable, Rect2,
look::{BorderStyle, Color, FontStyle, HorzAlign, Stroke},
},
render::{Device, Extreme, Pager, Params},
table::{Content, DrawCell},
},
spv::html::Markup,
};
const LINE_WIDTH: isize = LINE_SPACE / 2;
const LINE_SPACE: isize = SCALE as isize;
fn pxf_to_xr(x: f64) -> isize {
(x * (SCALE as f64 * 72.0 / 96.0)).round() as isize
}
#[derive(Clone, Debug)]
pub struct CairoFsmStyle {
pub size: Coord2,
pub min_break: EnumMap<Axis2, isize>,
pub font: FontDescription,
pub fg: Color,
pub use_system_colors: bool,
pub object_spacing: isize,
pub font_resolution: f64,
}
impl CairoFsmStyle {
fn new_layout(&self, context: &Context) -> Layout {
let pangocairo_context = pangocairo::functions::create_context(context);
pangocairo::functions::context_set_resolution(&pangocairo_context, self.font_resolution);
Layout::new(&pangocairo_context)
}
}
pub struct CairoFsm {
style: Arc<CairoFsmStyle>,
params: Params,
item: Arc<Item>,
layer_iterator: Option<Box<dyn Iterator<Item = SmallVec<[usize; 4]>>>>,
pager: Option<Pager>,
}
impl CairoFsm {
pub fn new(
style: Arc<CairoFsmStyle>,
printing: bool,
context: &Context,
item: Arc<Item>,
) -> Self {
let params = Params {
size: style.size,
font_size: {
let layout = style.new_layout(context);
layout.set_font_description(Some(&style.font));
layout.set_text("0");
let char_size = layout.size();
enum_map! {
Axis2::X => char_size.0 as isize,
Axis2::Y => char_size.1 as isize
}
},
line_widths: enum_map! {
Stroke::None => 0,
Stroke::Solid | Stroke::Dashed => LINE_WIDTH,
Stroke::Thick => LINE_WIDTH * 2,
Stroke::Thin => LINE_WIDTH / 2,
Stroke::Double => LINE_WIDTH * 2 + LINE_SPACE,
},
px_size: Some(px_to_xr(1)),
min_break: style.min_break,
supports_margins: true,
rtl: false,
printing,
can_adjust_break: false, can_scale: true,
};
let device = CairoDevice {
style: &style,
params: ¶ms,
context,
};
let item = if let Some(text) = item.details.as_text() {
Arc::new(Item::new(PivotTable::from(text.clone())))
} else {
item
};
let (layer_iterator, pager) = match &item.details {
Details::Table(pivot_table) => {
let mut layer_iterator = pivot_table.layers(printing);
let layer_indexes = layer_iterator.next();
(
Some(layer_iterator),
Some(Pager::new(
&device,
pivot_table,
layer_indexes.as_ref().map(|indexes| indexes.as_slice()),
)),
)
}
_ => (None, None),
};
Self {
style,
params,
item,
layer_iterator,
pager,
}
}
pub fn draw_slice(&mut self, context: &Context, space: isize) -> isize {
debug_assert!(self.params.printing);
context.save().unwrap();
let used = match &self.item.details {
Details::Table(_) => self.draw_table(context, space),
_ => 0,
};
context.restore().unwrap();
used
}
fn draw_table(&mut self, context: &Context, space: isize) -> isize {
let pivot_table = self.item.details.as_table().unwrap();
let Some(pager) = &mut self.pager else {
return 0;
};
let mut device = CairoDevice {
style: &self.style,
params: &self.params,
context,
};
let mut used = pager.draw_next(&mut device, space);
if pager.has_next(&device).is_none() {
if let Some(layer_indexes) = self.layer_iterator.as_mut().unwrap().next() {
self.pager = Some(Pager::new(
&device,
pivot_table,
Some(layer_indexes.as_slice()),
));
if pivot_table.style.look.paginate_layers {
used = space;
} else {
used += self.style.object_spacing;
}
} else {
self.pager = None;
}
}
used.min(space)
}
pub fn is_done(&self) -> bool {
match &self.item.details {
Details::Table(_) => self.pager.is_none(),
_ => true,
}
}
}
fn xr_clip(context: &Context, clip: &Rect2) {
if clip[Axis2::X].end != isize::MAX || clip[Axis2::Y].end != isize::MAX {
let x0 = xr_to_pt(clip[Axis2::X].start);
let y0 = xr_to_pt(clip[Axis2::Y].start);
let x1 = xr_to_pt(clip[Axis2::X].end);
let y1 = xr_to_pt(clip[Axis2::Y].end);
context.rectangle(x0, y0, x1 - x0, y1 - y0);
context.clip();
}
}
fn xr_set_color(context: &Context, color: Color) {
fn as_frac(x: u8) -> f64 {
x as f64 / 255.0
}
context.set_source_rgba(
as_frac(color.r),
as_frac(color.g),
as_frac(color.b),
as_frac(color.alpha),
);
}
fn xr_fill_rectangle(context: &Context, rectangle: Rect2) {
context.new_path();
context.set_line_width(xr_to_pt(LINE_WIDTH));
let x0 = xr_to_pt(rectangle[Axis2::X].start);
let y0 = xr_to_pt(rectangle[Axis2::Y].start);
let width = xr_to_pt(rectangle[Axis2::X].len() as isize);
let height = xr_to_pt(rectangle[Axis2::Y].len() as isize);
context.rectangle(x0, y0, width, height);
context.fill().unwrap();
}
fn margin(cell: &DrawCell, axis: Axis2) -> isize {
px_to_xr(cell.cell_style.margins[axis].iter().sum::<i32>() as isize)
}
pub fn parse_font_style(font_style: &FontStyle) -> FontDescription {
let font = font_style.font.as_str();
let font = if font.eq_ignore_ascii_case("Monospaced") {
"Monospace"
} else {
font
};
let mut font_desc = FontDescription::from_string(font);
if !font_desc.set_fields().contains(FontMask::SIZE) {
let default_size = if font_style.size != 0 {
font_style.size * 1000
} else {
10_000
};
font_desc.set_size(((default_size as f64 / 1000.0) * (SCALE as f64)) as i32);
}
font_desc.set_weight(if font_style.bold {
Weight::Bold
} else {
Weight::Normal
});
font_desc.set_style(if font_style.italic {
pango::Style::Italic
} else {
pango::Style::Normal
});
font_desc
}
fn avoid_decimal_split(mut s: String) -> String {
if let Some(position) = s.find(['.', ',']) {
let followed_by_digit = s[position + 1..]
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit());
let not_preceded_by_digit = s[..position]
.chars()
.next_back()
.is_none_or(|c| !c.is_ascii_digit());
if followed_by_digit && not_preceded_by_digit {
s.insert(position + 1, '\u{2060}');
}
}
s
}
impl<'a, 'b> DrawCell<'a, 'b> {
pub(crate) fn layout(&self, bb: &Rect2, layout: &mut Layout, default_font: &FontDescription) {
let mut bb = bb.clone();
layout.set_attributes(None);
let parsed_font;
let font = if !self.font_style.font.is_empty() {
parsed_font = parse_font_style(&self.font_style);
&parsed_font
} else {
default_font
};
layout.set_font_description(Some(font));
let (body_display, suffixes) = self.display().split();
let horz_align = self.horz_align(&body_display);
let (mut body, mut attrs) = if let Some(markup) = body_display.markup() {
let (body, attrs) = Markup::to_pango(markup, self.substitutions);
(body, Some(attrs))
} else {
(avoid_decimal_split(body_display.to_string()), None)
};
if let Some(decimal_offset) = horz_align.decimal_offset()
&& !self.rotate
&& let Some(decimal) = body_display.decimal()
&& let Some(index) = body.rfind(char::from(decimal))
{
layout.set_text(&body[index..]);
layout.set_width(-1);
bb[Axis2::X].end -= pxf_to_xr(decimal_offset).saturating_sub(layout.size().0 as isize);
}
if self.font_style.underline {
attrs
.get_or_insert_default()
.insert(AttrInt::new_underline(Underline::Single));
}
if !suffixes.is_empty() {
let subscript_ofs = body.len();
#[allow(unstable_name_collisions)]
body.extend(suffixes.subscripts().intersperse(","));
let has_subscripts = subscript_ofs != body.len();
let footnote_ofs = body.len();
for (index, footnote) in suffixes.footnotes().enumerate() {
if index > 0 {
body.push(',');
}
write!(&mut body, "{footnote}").unwrap();
}
let has_footnotes = footnote_ofs != body.len();
if has_footnotes && horz_align == HorzAlign::Right {
layout.set_text(&body[footnote_ofs..]);
let footnote_attrs = AttrList::new();
footnote_attrs.insert(AttrFloat::new_scale(SCALE_SMALL));
footnote_attrs.insert(AttrInt::new_rise(3000));
layout.set_attributes(Some(&footnote_attrs));
let footnote_width = layout.size().0 as isize;
let right_margin = px_to_xr(self.cell_style.margins[Axis2::X][1] as isize);
let footnote_adjustment = min(footnote_width, right_margin);
if self.rotate {
bb[Axis2::X].end = bb[Axis2::X].end.saturating_sub(footnote_adjustment);
} else {
bb[Axis2::X].end = bb[Axis2::X].end.saturating_add(footnote_adjustment);
}
layout.set_attributes(None);
}
fn with_start<T: DerefMut<Target = Attribute>>(index: usize, mut attr: T) -> T {
attr.deref_mut().set_start_index(index.try_into().unwrap());
attr
}
fn with_end<T: DerefMut<Target = Attribute>>(index: usize, mut attr: T) -> T {
attr.deref_mut().set_end_index(index.try_into().unwrap());
attr
}
let attrs = attrs.get_or_insert_default();
attrs.insert(with_start(subscript_ofs, AttrFontDesc::new(font)));
attrs.insert(with_start(subscript_ofs, AttrFloat::new_scale(SCALE_SMALL)));
if has_subscripts {
attrs.insert(with_start(
subscript_ofs,
with_end(footnote_ofs, AttrInt::new_rise(-3000)),
));
}
if has_footnotes {
let rise = 3000; attrs.insert(with_start(footnote_ofs, AttrInt::new_rise(rise)));
}
}
layout.set_attributes(attrs.as_ref());
layout.set_text(&body);
layout.set_alignment(horz_align.into());
if bb[Axis2::X].end == isize::MAX {
layout.set_width(-1);
} else {
layout.set_width(bb[Axis2::X].len() as i32);
}
}
pub(crate) fn draw(
&self,
bb: &Rect2,
layout: &Layout,
clip: Option<&Rect2>,
context: &Context,
) {
context.save().unwrap();
if !self.rotate
&& let Some(clip) = clip
{
xr_clip(context, clip);
}
if self.rotate {
let extra = (bb[Axis2::X].len() as isize - layout.size().1 as isize).max(0);
let halign_offset = extra / 2;
context.translate(
xr_to_pt(bb[Axis2::X].start + halign_offset),
xr_to_pt(bb[Axis2::Y].end),
);
context.rotate(-PI / 2.0);
} else {
context.translate(xr_to_pt(bb[Axis2::X].start), xr_to_pt(bb[Axis2::Y].start));
}
show_layout(context, &layout);
context.restore().unwrap();
}
}
struct CairoDevice<'a> {
style: &'a CairoFsmStyle,
params: &'a Params,
context: &'a Context,
}
impl CairoDevice<'_> {
fn measure_cell(&self, cell: &DrawCell, bb: Rect2) -> Coord2 {
let mut layout = self.style.new_layout(self.context);
cell.layout(&bb, &mut layout, &self.style.font);
let (width, height) = layout.size();
Coord2::new(width as isize, height as isize)
}
fn cell_draw(&self, cell: &DrawCell, bb: Rect2, clip: &Rect2) {
let mut layout = self.style.new_layout(self.context);
cell.layout(&bb, &mut layout, &self.style.font);
cell.draw(&bb, &layout, Some(clip), &self.context);
}
fn do_draw_line(
&self,
x0: isize,
y0: isize,
x1: isize,
y1: isize,
stroke: Stroke,
color: Color,
) {
self.context.new_path();
self.context.set_line_width(xr_to_pt(match stroke {
Stroke::Thick => LINE_WIDTH * 2,
Stroke::Thin => LINE_WIDTH / 2,
_ => LINE_WIDTH,
}));
self.context.move_to(xr_to_pt(x0), xr_to_pt(y0));
self.context.line_to(xr_to_pt(x1), xr_to_pt(y1));
if !self.style.use_system_colors {
xr_set_color(self.context, color);
}
if stroke == Stroke::Dashed {
self.context.set_dash(&[2.0], 0.0);
let _ = self.context.stroke();
self.context.set_dash(&[], 0.0);
} else {
let _ = self.context.stroke();
}
}
}
impl Device for CairoDevice<'_> {
fn params(&self) -> &Params {
self.params
}
fn measure_cell_width(&self, cell: &DrawCell) -> EnumMap<Extreme, isize> {
fn add_margins(cell: &DrawCell, width: isize) -> isize {
if width > 0 {
width + margin(cell, Axis2::X)
} else {
0
}
}
enum_map![
Extreme::Min => {
let bb = Rect2::new(0..1, 0..isize::MAX);
add_margins(cell, self.measure_cell(cell, bb).x())
}
Extreme::Max => {
let bb = Rect2::new(0..isize::MAX, 0..isize::MAX);
add_margins(cell, self.measure_cell(cell, bb).x())
},
]
}
fn measure_cell_height(&self, cell: &DrawCell, width: isize) -> isize {
let bb = Rect2::new(
0..width.saturating_sub(margin(cell, Axis2::X)),
0..isize::MAX,
);
self.measure_cell(cell, bb).y() + margin(cell, Axis2::Y)
}
fn adjust_break(&self, _cell: &Content, _size: Coord2) -> isize {
todo!()
}
fn draw_line(&mut self, bb: Rect2, styles: EnumMap<Axis2, [BorderStyle; 2]>, _bg: Color) {
let x0 = bb[Axis2::X].start;
let y0 = bb[Axis2::Y].start;
let x3 = bb[Axis2::X].end;
let y3 = bb[Axis2::Y].end;
let top = styles[Axis2::X][0].stroke;
let bottom = styles[Axis2::X][1].stroke;
let left = styles[Axis2::Y][0].stroke;
let right = styles[Axis2::Y][1].stroke;
let top_color = styles[Axis2::X][0].color;
let bottom_color = styles[Axis2::X][1].color;
let left_color = styles[Axis2::Y][0].color;
let right_color = styles[Axis2::Y][1].color;
let double_line_ofs = (LINE_SPACE + LINE_WIDTH) / 2;
let double_vert = top == Stroke::Double || bottom == Stroke::Double;
let double_horz = left == Stroke::Double || right == Stroke::Double;
let shorten_y1_lines = top == Stroke::Double;
let shorten_y2_lines = bottom == Stroke::Double;
let shorten_yc_line = shorten_y1_lines && shorten_y2_lines;
let horz_line_ofs = if double_vert { double_line_ofs } else { 0 };
let xc = (x0 + x3) / 2;
let x1 = xc - horz_line_ofs;
let x2 = xc + horz_line_ofs;
let shorten_x1_lines = left == Stroke::Double;
let shorten_x2_lines = right == Stroke::Double;
let shorten_xc_line = shorten_x1_lines && shorten_x2_lines;
let vert_line_ofs = if double_horz { double_line_ofs } else { 0 };
let yc = (y0 + y3) / 2;
let y1 = yc - vert_line_ofs;
let y2 = yc + vert_line_ofs;
let horz_lines: SmallVec<[_; 2]> = if double_horz {
smallvec![(y1, shorten_y1_lines), (y2, shorten_y2_lines)]
} else {
smallvec![(yc, shorten_yc_line)]
};
for (y, shorten) in horz_lines {
if left != Stroke::None
&& right != Stroke::None
&& !shorten
&& left_color == right_color
{
self.do_draw_line(x0, y, x3, y, left, left_color);
} else {
if left != Stroke::None {
self.do_draw_line(x0, y, if shorten { x1 } else { x2 }, y, left, left_color);
}
if right != Stroke::None {
self.do_draw_line(if shorten { x2 } else { x1 }, y, x3, y, right, right_color);
}
}
}
let vert_lines: SmallVec<[_; 2]> = if double_vert {
smallvec![(x1, shorten_x1_lines), (x2, shorten_x2_lines)]
} else {
smallvec![(xc, shorten_xc_line)]
};
for (x, shorten) in vert_lines {
if top != Stroke::None
&& bottom != Stroke::None
&& !shorten
&& top_color == bottom_color
{
self.do_draw_line(x, y0, x, y3, top, top_color);
} else {
if top != Stroke::None {
self.do_draw_line(x, y0, x, if shorten { y1 } else { y2 }, top, top_color);
}
if bottom != Stroke::None {
self.do_draw_line(
x,
if shorten { y2 } else { y1 },
x,
y3,
bottom,
bottom_color,
);
}
}
}
}
fn draw_cell(
&mut self,
draw_cell: &DrawCell,
mut bb: Rect2,
valign_offset: isize,
spill: EnumMap<Axis2, [isize; 2]>,
clip: &Rect2,
) {
let bg = draw_cell.font_style.bg;
if (bg.r != 255 || bg.g != 255 || bg.b != 255) && bg.alpha != 0 {
self.context.save().unwrap();
let bg_clip = Rect2::from_fn(|axis| {
let start = if bb[axis].start == clip[axis].start {
clip[axis].start.saturating_sub(spill[axis][0])
} else {
clip[axis].start
};
let end = if bb[axis].end == clip[axis].end {
clip[axis].end + spill[axis][1]
} else {
clip[axis].end
};
start..end
});
xr_clip(self.context, &bg_clip);
xr_set_color(self.context, bg);
let x0 = bb[Axis2::X].start.saturating_sub(spill[Axis2::X][0]);
let y0 = bb[Axis2::Y].start.saturating_sub(spill[Axis2::X][1]);
let x1 = bb[Axis2::X].end + spill[Axis2::X][1];
let y1 = bb[Axis2::Y].end + spill[Axis2::Y][1];
xr_fill_rectangle(self.context, Rect2::new(x0..x1, y0..y1));
self.context.restore().unwrap();
}
if !self.style.use_system_colors {
xr_set_color(self.context, draw_cell.font_style.fg);
}
self.context.save().unwrap();
bb[Axis2::Y].start += valign_offset;
for axis in [Axis2::X, Axis2::Y] {
bb[axis].start += px_to_xr(draw_cell.cell_style.margins[axis][0] as isize);
bb[axis].end -= px_to_xr(draw_cell.cell_style.margins[axis][1] as isize);
}
if bb[Axis2::X].start < bb[Axis2::X].end && bb[Axis2::Y].start < bb[Axis2::Y].end {
self.cell_draw(draw_cell, bb, clip);
}
self.context.restore().unwrap();
}
fn scale(&mut self, factor: f64) {
self.context.scale(factor, factor);
}
}