1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! Print-preview pagination (steps 10.5/15.2, items H31/H41).
//!
//! The report design acts as the page template. Since the expression
//! service, pagination lives in [`crate::report_bands`]: pages carry a
//! band kind (`Detail`, or the `GroupHeader`/`GroupFooter` bands of a
//! grouped report) and `=expression` controls evaluate per record or per
//! group. Values render through the shared presentation layer, so
//! preview text matches the datasheet.
use crate::canvas::Rect;
use crate::report::ReportDef;
use crate::schema::{FieldValue, TableDef};
/// Which band a preview page renders.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Band {
/// One record's values (also the only band of ungrouped reports).
Detail,
/// The group's key value, rendered once per group.
GroupHeader,
/// The group's aggregate expressions, rendered once per group.
GroupFooter,
}
/// One positioned render item in page coordinates.
#[derive(Debug, Clone, PartialEq)]
pub struct PreviewItem {
/// The rendered text.
pub text: String,
/// Page placement (design coordinates, points).
pub rect: Rect,
}
/// One preview page: the design template filled with one band's values.
#[derive(Debug, Clone, PartialEq)]
pub struct PreviewPage {
/// The band this page renders.
pub band: Band,
/// Render items in control (paint) order.
pub items: Vec<PreviewItem>,
}
/// Paginates a report design over source rows — grouped reports render
/// header/footer bands, ungrouped ones stay single-record pages.
pub fn paginate(
report: &ReportDef,
source: Option<&TableDef>,
rows: &[Vec<FieldValue>],
) -> Result<Vec<PreviewPage>, String> {
crate::report_bands::paginate(report, source, rows)
}
/// Renders a bound field against one row (unknown fields and `Null`
/// render empty); the expression path lives in `report_bands`.
pub(crate) fn bound_text(
source: Option<&TableDef>,
row: Option<&Vec<FieldValue>>,
field: &str,
) -> String {
let (Some(source), Some(row)) = (source, row) else {
return String::new();
};
let Some(column) = source
.fields
.iter()
.position(|candidate| candidate.name.eq_ignore_ascii_case(field))
else {
return String::new();
};
match row.get(column) {
Some(FieldValue::Null) | None => String::new(),
Some(value) => crate::present::display_value(value),
}
}
impl crate::core::Core {
/// Builds the print preview of one report: resolves the whitelisted
/// source, reads its rows through the port and paginates. Failures
/// are precise (unknown group/aggregate fields, broken expressions).
pub fn report_preview(&self, report: &str) -> Result<Vec<PreviewPage>, String> {
let db = self.state.database.as_ref().ok_or("no database is open")?;
let def = db
.reports
.iter()
.find(|r| r.name.to_lowercase() == report.to_lowercase())
.ok_or_else(|| format!("report {report:?} not found"))?;
let source = db
.tables
.iter()
.find(|t| t.name.eq_ignore_ascii_case(&def.source));
let rows = match source {
Some(table) => self
.port
.read_rows(
&table.name,
crate::port::RowRange {
start: 0,
len: usize::MAX,
},
)
.map_err(|e| e.to_string())?,
None => Vec::new(),
};
paginate(def, source, &rows)
}
}