1use std::collections::HashMap;
5
6use crate::build::ParaBuilder;
7use crate::font::FontHandle;
8use crate::model::{Align, Color, Inline, TextStyle};
9
10#[derive(Clone, Copy, Debug)]
12pub struct Insets {
13 pub top: f32,
15 pub right: f32,
17 pub bottom: f32,
19 pub left: f32,
21}
22
23impl Insets {
24 pub const fn all(v: f32) -> Self {
26 Self { top: v, right: v, bottom: v, left: v }
27 }
28 pub const fn symmetric(v: f32, h: f32) -> Self {
30 Self { top: v, right: h, bottom: v, left: h }
31 }
32}
33
34#[derive(Clone, Copy, Debug)]
36pub struct CodePalette {
37 pub keyword: Color,
39 pub literal: Color,
41 pub string: Color,
43 pub comment: Color,
45}
46
47#[derive(Clone, Debug)]
49pub struct Theme {
50 pub background: Color,
52 pub text: Color,
54 pub accent: Color,
56 pub muted: Color,
58 pub code_bg: Color,
60 pub code_text: Color,
62 pub code_palette: CodePalette,
64 pub highlight: Color,
66 pub border: Color,
68 pub font_sans: String,
70 pub font_serif: String,
72 pub font_mono: String,
74 pub font_kai: String,
76 pub font_emoji: String,
79 pub base_size: f32,
81 pub line_height: f32,
83 pub heading_scale: [f32; 6],
85}
86
87impl Theme {
88 pub fn light() -> Self {
90 Self {
91 background: Color::rgb(0xff, 0xff, 0xff),
92 text: Color::rgb(0x1f, 0x23, 0x28),
93 accent: Color::rgb(0x25, 0x63, 0xeb),
94 muted: Color::rgb(0x6e, 0x77, 0x81),
95 code_bg: Color::rgb(0xf3, 0xf4, 0xf6),
96 code_text: Color::rgb(0x1f, 0x23, 0x28),
97 code_palette: CodePalette {
98 keyword: Color::rgb(0xcf, 0x22, 0x2e),
99 literal: Color::rgb(0x05, 0x50, 0xae),
100 string: Color::rgb(0x0a, 0x30, 0x69),
101 comment: Color::rgb(0x6e, 0x77, 0x81),
102 },
103 highlight: Color::rgb(0xff, 0xf1, 0xa8),
104 border: Color::rgb(0xe5, 0xe7, 0xeb),
105 ..Self::common()
106 }
107 }
108
109 pub fn dark() -> Self {
111 Self {
112 background: Color::rgb(0x0d, 0x11, 0x17),
113 text: Color::rgb(0xe6, 0xed, 0xf3),
114 accent: Color::rgb(0x58, 0xa6, 0xff),
115 muted: Color::rgb(0x8b, 0x94, 0x9e),
116 code_bg: Color::rgb(0x16, 0x1b, 0x22),
117 code_text: Color::rgb(0xe6, 0xed, 0xf3),
118 code_palette: CodePalette {
119 keyword: Color::rgb(0xff, 0x7b, 0x72),
120 literal: Color::rgb(0x79, 0xc0, 0xff),
121 string: Color::rgb(0xa5, 0xd6, 0xff),
122 comment: Color::rgb(0x8b, 0x94, 0x9e),
123 },
124 highlight: Color::rgb(0x57, 0x4a, 0x1a),
125 border: Color::rgb(0x30, 0x36, 0x3d),
126 ..Self::common()
127 }
128 }
129
130 fn common() -> Self {
133 Self {
134 background: Color::rgb(0, 0, 0),
135 text: Color::rgb(0, 0, 0),
136 accent: Color::rgb(0, 0, 0),
137 muted: Color::rgb(0, 0, 0),
138 code_bg: Color::rgb(0, 0, 0),
139 code_text: Color::rgb(0, 0, 0),
140 code_palette: CodePalette {
141 keyword: Color::rgb(0, 0, 0),
142 literal: Color::rgb(0, 0, 0),
143 string: Color::rgb(0, 0, 0),
144 comment: Color::rgb(0, 0, 0),
145 },
146 highlight: Color::rgb(0, 0, 0),
147 border: Color::rgb(0, 0, 0),
148 font_sans: "Noto Sans SC".to_string(), font_serif: "Noto Serif SC".to_string(), font_mono: "JetBrains Mono".to_string(), font_kai: "LXGW WenKai GB".to_string(), font_emoji: "Noto Color Emoji".to_string(), base_size: 30.0,
154 line_height: 1.5,
155 heading_scale: [2.0, 1.6, 1.35, 1.15, 1.0, 0.9],
156 }
157 }
158}
159
160impl Default for Theme {
161 fn default() -> Self {
162 Self::light()
163 }
164}
165
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub enum OutputFormat {
169 Png,
171 PngFast,
173 Webp,
175 WebpOrPng,
178}
179
180#[derive(Clone)]
182pub struct RenderOptions {
183 pub width: f32,
185 pub padding: Insets,
187 pub scale: f32,
189 pub theme: Theme,
191 pub fonts: FontHandle,
193 pub format: OutputFormat,
195 pub images: HashMap<String, Vec<u8>>,
197 pub header: Option<PageChrome>,
199 pub footer: Option<PageChrome>,
201}
202
203#[derive(Clone, Debug)]
207pub struct PageChrome {
208 pub inlines: Vec<Inline>,
210 pub trailing: Option<Vec<Inline>>,
213 pub align: Align,
215 pub color: Option<Color>,
217 pub size: f32,
219 pub rule: bool,
221 pub band: Option<Color>,
224}
225
226impl PageChrome {
227 pub fn new(text: impl Into<String>) -> Self {
229 Self::from_inlines(vec![Inline::Text { text: text.into(), style: TextStyle::default() }])
230 }
231
232 pub fn rich<R>(f: impl FnOnce(&mut ParaBuilder) -> R) -> Self {
235 let mut pb = ParaBuilder::new();
236 let _ = f(&mut pb);
237 Self::from_inlines(pb.into_inlines())
238 }
239
240 fn from_inlines(inlines: Vec<Inline>) -> Self {
241 Self { inlines, trailing: None, align: Align::Left, color: None, size: 0.72, rule: true, band: None }
242 }
243
244 pub fn trailing<R>(mut self, f: impl FnOnce(&mut ParaBuilder) -> R) -> Self {
246 let mut pb = ParaBuilder::new();
247 let _ = f(&mut pb);
248 self.trailing = Some(pb.into_inlines());
249 self
250 }
251
252 pub fn band(mut self, hex: &str) -> Self {
254 if let Some(c) = Color::hex(hex) {
255 self.band = Some(c);
256 }
257 self
258 }
259 pub fn align(mut self, a: Align) -> Self {
261 self.align = a;
262 self
263 }
264 pub fn color(mut self, hex: &str) -> Self {
266 if let Some(c) = Color::hex(hex) {
267 self.color = Some(c);
268 }
269 self
270 }
271 pub fn size(mut self, mult: f32) -> Self {
273 if mult.is_finite() && mult > 0.0 {
274 self.size = mult;
275 }
276 self
277 }
278 pub fn no_rule(mut self) -> Self {
280 self.rule = false;
281 self
282 }
283}
284
285impl Default for RenderOptions {
286 fn default() -> Self {
287 Self {
288 width: 960.0,
289 padding: Insets::symmetric(32.0, 40.0),
290 scale: 2.0,
291 theme: Theme::light(),
292 fonts: FontHandle::shared_default(),
293 format: OutputFormat::Png,
294 images: HashMap::new(),
295 header: None,
296 footer: None,
297 }
298 }
299}
300
301impl RenderOptions {
302 pub fn with_width(mut self, w: f32) -> Self {
304 self.width = w;
305 self
306 }
307 pub fn with_padding(mut self, p: Insets) -> Self {
309 self.padding = p;
310 self
311 }
312 pub fn with_theme(mut self, t: Theme) -> Self {
314 self.theme = t;
315 self
316 }
317 pub fn with_fonts(mut self, f: FontHandle) -> Self {
319 self.fonts = f;
320 self
321 }
322 pub fn with_scale(mut self, s: f32) -> Self {
324 self.scale = s.clamp(0.25, 8.0);
325 self
326 }
327 pub fn fast(self) -> Self {
329 self.with_scale(1.0)
330 }
331 pub fn standard(self) -> Self {
333 self.with_scale(1.5)
334 }
335 pub fn sharp(self) -> Self {
337 self.with_scale(2.0)
338 }
339 pub fn ultra(self) -> Self {
341 self.with_scale(3.0)
342 }
343 pub fn with_header(self, text: impl Into<String>) -> Self {
345 self.with_header_chrome(PageChrome::new(text))
346 }
347 pub fn with_header_chrome(mut self, c: PageChrome) -> Self {
349 self.header = Some(c);
350 self
351 }
352 pub fn with_footer(self, text: impl Into<String>) -> Self {
354 self.with_footer_chrome(PageChrome::new(text).align(Align::Center))
355 }
356 pub fn with_footer_chrome(mut self, c: PageChrome) -> Self {
358 self.footer = Some(c);
359 self
360 }
361 pub fn with_format(mut self, f: OutputFormat) -> Self {
363 self.format = f;
364 self
365 }
366 pub fn png(self) -> Self {
368 self.with_format(OutputFormat::Png)
369 }
370 pub fn png_fast(self) -> Self {
372 self.with_format(OutputFormat::PngFast)
373 }
374 pub fn webp(self) -> Self {
376 self.with_format(OutputFormat::Webp)
377 }
378 pub fn webp_or_png(self) -> Self {
380 self.with_format(OutputFormat::WebpOrPng)
381 }
382}