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 {
242 inlines,
243 trailing: None,
244 align: Align::Left,
245 color: None,
246 size: 0.72,
247 rule: true,
248 band: None,
249 }
250 }
251
252 pub fn trailing<R>(mut self, f: impl FnOnce(&mut ParaBuilder) -> R) -> Self {
254 let mut pb = ParaBuilder::new();
255 let _ = f(&mut pb);
256 self.trailing = Some(pb.into_inlines());
257 self
258 }
259
260 pub fn band(mut self, hex: &str) -> Self {
262 if let Some(c) = Color::hex(hex) {
263 self.band = Some(c);
264 }
265 self
266 }
267 pub fn align(mut self, a: Align) -> Self {
269 self.align = a;
270 self
271 }
272 pub fn color(mut self, hex: &str) -> Self {
274 if let Some(c) = Color::hex(hex) {
275 self.color = Some(c);
276 }
277 self
278 }
279 pub fn size(mut self, mult: f32) -> Self {
281 if mult.is_finite() && mult > 0.0 {
282 self.size = mult;
283 }
284 self
285 }
286 pub fn no_rule(mut self) -> Self {
288 self.rule = false;
289 self
290 }
291}
292
293impl Default for RenderOptions {
294 fn default() -> Self {
295 Self {
296 width: 960.0,
297 padding: Insets::symmetric(32.0, 40.0),
298 scale: 2.0,
299 theme: Theme::light(),
300 fonts: FontHandle::shared_default(),
301 format: OutputFormat::Png,
302 images: HashMap::new(),
303 header: None,
304 footer: None,
305 }
306 }
307}
308
309impl RenderOptions {
310 pub fn with_width(mut self, w: f32) -> Self {
312 self.width = w;
313 self
314 }
315 pub fn with_padding(mut self, p: Insets) -> Self {
317 self.padding = p;
318 self
319 }
320 pub fn with_theme(mut self, t: Theme) -> Self {
322 self.theme = t;
323 self
324 }
325 pub fn with_fonts(mut self, f: FontHandle) -> Self {
327 self.fonts = f;
328 self
329 }
330 pub fn with_scale(mut self, s: f32) -> Self {
332 self.scale = s.clamp(0.25, 8.0);
333 self
334 }
335 pub fn fast(self) -> Self {
337 self.with_scale(1.0)
338 }
339 pub fn standard(self) -> Self {
341 self.with_scale(1.5)
342 }
343 pub fn sharp(self) -> Self {
345 self.with_scale(2.0)
346 }
347 pub fn ultra(self) -> Self {
349 self.with_scale(3.0)
350 }
351 pub fn with_header(self, text: impl Into<String>) -> Self {
353 self.with_header_chrome(PageChrome::new(text))
354 }
355 pub fn with_header_chrome(mut self, c: PageChrome) -> Self {
357 self.header = Some(c);
358 self
359 }
360 pub fn with_footer(self, text: impl Into<String>) -> Self {
362 self.with_footer_chrome(PageChrome::new(text).align(Align::Center))
363 }
364 pub fn with_footer_chrome(mut self, c: PageChrome) -> Self {
366 self.footer = Some(c);
367 self
368 }
369 pub fn with_format(mut self, f: OutputFormat) -> Self {
371 self.format = f;
372 self
373 }
374 pub fn png(self) -> Self {
376 self.with_format(OutputFormat::Png)
377 }
378 pub fn png_fast(self) -> Self {
380 self.with_format(OutputFormat::PngFast)
381 }
382 pub fn webp(self) -> Self {
384 self.with_format(OutputFormat::Webp)
385 }
386 pub fn webp_or_png(self) -> Self {
388 self.with_format(OutputFormat::WebpOrPng)
389 }
390}