1use std::collections::HashMap;
15use std::sync::Arc;
16
17use peniko::{Blob, ImageAlphaType, ImageData, ImageFormat};
18
19use crate::Problem;
20use crate::fonts::{FontFile, FontFiles, FontId};
21use crate::parse::{self, Pos};
22use crate::tree::{Children, NodeId, Order, OrderKey};
23
24#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Rgba {
27 pub r: u8,
28 pub g: u8,
29 pub b: u8,
30 pub a: u8,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
35pub enum Theme {
36 #[default]
37 Light,
38 Dark,
39}
40
41impl Theme {
42 pub fn named(name: &str) -> Option<Theme> {
44 match name {
45 "light" => Some(Theme::Light),
46 "dark" => Some(Theme::Dark),
47 _ => None,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq)]
55pub struct Color {
56 pub light: Rgba,
57 pub dark: Rgba,
58}
59
60impl Color {
61 pub const fn fixed(c: Rgba) -> Color {
63 Color { light: c, dark: c }
64 }
65
66 pub fn get(self, theme: Theme) -> Rgba {
68 match theme {
69 Theme::Light => self.light,
70 Theme::Dark => self.dark,
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq)]
76pub enum Size {
77 Px(f32),
78 Hug,
79 Fill,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq)]
83pub enum Layout {
84 None,
85 Row,
86 Column,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq)]
90pub enum Align {
91 Start,
92 Center,
93 End,
94 SpaceBetween,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq)]
98pub enum Kind {
99 Frame,
100 Text,
101 Rect,
102 Ellipse,
103 Vector,
104 Image,
105}
106
107#[derive(Debug, Clone, PartialEq)]
109pub enum Fill {
110 Solid(Color),
111 Linear {
114 angle: f32,
115 stops: Vec<(Color, f32)>,
116 },
117}
118
119#[derive(Debug, Clone, Copy, PartialEq)]
120pub enum TextAlign {
121 Left,
122 Center,
123 Right,
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub struct Vector {
129 pub d: String,
130 pub view_box: [f32; 4],
132}
133
134#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
138pub struct ImageId(pub [u8; 32]);
139
140impl ImageId {
141 pub fn of(bytes: &[u8]) -> ImageId {
142 use sha2::Digest;
143 ImageId(sha2::Sha256::digest(bytes).into())
144 }
145}
146
147impl std::fmt::Debug for ImageId {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 for b in &self.0[..6] {
150 write!(f, "{b:02x}")?;
151 }
152 Ok(())
153 }
154}
155
156pub const MAX_IMAGE_SIDE: u32 = 4096;
161
162#[derive(Debug)]
165pub struct ImageFile {
166 pub id: ImageId,
167 pub bytes: Vec<u8>,
168 pub width: u32,
170 pub height: u32,
171 pub pixels: Arc<Vec<u8>>,
173 pub pixels_width: u32,
174 pub pixels_height: u32,
175}
176
177impl ImageFile {
178 pub fn new(bytes: Vec<u8>) -> Result<ImageFile, String> {
180 ImageFile::with_id(ImageId::of(&bytes), bytes)
181 }
182
183 pub fn with_id(id: ImageId, bytes: Vec<u8>) -> Result<ImageFile, String> {
185 let decoded = decode(&bytes)?;
186 let (width, height) = (decoded.width, decoded.height);
187 let (pixels, pixels_width, pixels_height) =
188 fit(decoded.data.data(), width, height, MAX_IMAGE_SIDE);
189 Ok(ImageFile {
190 id,
191 bytes,
192 width,
193 height,
194 pixels: Arc::new(pixels),
195 pixels_width,
196 pixels_height,
197 })
198 }
199
200 pub fn image_data(&self) -> ImageData {
203 ImageData {
204 data: Blob::new(self.pixels.clone()),
205 format: ImageFormat::Rgba8,
206 alpha_type: ImageAlphaType::Alpha,
207 width: self.pixels_width,
208 height: self.pixels_height,
209 }
210 }
211}
212
213pub fn fit(rgba: &[u8], width: u32, height: u32, max: u32) -> (Vec<u8>, u32, u32) {
218 if width <= max && height <= max {
219 return (rgba.to_vec(), width, height);
220 }
221 let scale = max as f64 / width.max(height) as f64;
222 let w = ((width as f64 * scale).round() as u32).clamp(1, max);
223 let h = ((height as f64 * scale).round() as u32).clamp(1, max);
224 let weights = |from: u32, to: u32| -> Vec<Vec<(usize, f32)>> {
227 let ratio = from as f64 / to as f64;
228 (0..to)
229 .map(|o| {
230 let (a, b) = (o as f64 * ratio, (o + 1) as f64 * ratio);
231 (a.floor() as usize..(b.ceil() as usize).min(from as usize))
232 .map(|i| {
233 let cover = (b.min(i as f64 + 1.0) - a.max(i as f64)) / ratio;
234 (i, cover as f32)
235 })
236 .filter(|&(_, c)| c > 0.0)
237 .collect()
238 })
239 .collect()
240 };
241 let (across, down) = (weights(width, w), weights(height, h));
242 let mut rows = vec![0f32; 4 * w as usize * height as usize];
244 for y in 0..height as usize {
245 let line = &rgba[4 * y * width as usize..][..4 * width as usize];
246 for (x, ws) in across.iter().enumerate() {
247 let mut sum = [0f32; 4];
248 for &(i, c) in ws {
249 let p = &line[4 * i..][..4];
250 let a = p[3] as f32 * c;
251 sum[0] += p[0] as f32 * a;
252 sum[1] += p[1] as f32 * a;
253 sum[2] += p[2] as f32 * a;
254 sum[3] += a;
255 }
256 rows[4 * (y * w as usize + x)..][..4].copy_from_slice(&sum);
257 }
258 }
259 let mut out = vec![0u8; 4 * w as usize * h as usize];
260 for (y, ws) in down.iter().enumerate() {
261 for x in 0..w as usize {
262 let mut sum = [0f32; 4];
263 for &(i, c) in ws {
264 let p = &rows[4 * (i * w as usize + x)..][..4];
265 for k in 0..4 {
266 sum[k] += p[k] * c;
267 }
268 }
269 let px = &mut out[4 * (y * w as usize + x)..][..4];
270 if sum[3] > 0.0 {
271 for k in 0..3 {
272 px[k] = (sum[k] / sum[3]).round().clamp(0.0, 255.0) as u8;
273 }
274 px[3] = sum[3].round().clamp(0.0, 255.0) as u8;
275 }
276 }
277 }
278 (out, w, h)
279}
280
281#[derive(Debug, Clone)]
285pub struct Origin {
286 pub file: String,
287 pub pos: Pos,
288}
289
290impl Default for Origin {
291 fn default() -> Origin {
292 Origin {
293 file: String::new(),
294 pos: Pos { line: 1, col: 1 },
295 }
296 }
297}
298
299impl PartialEq for Origin {
300 fn eq(&self, _: &Origin) -> bool {
301 true
302 }
303}
304
305#[derive(Debug, Clone, PartialEq)]
308pub struct Image {
309 pub src: String,
310 pub id: ImageId,
311 pub at: Origin,
313}
314
315pub fn decode(data: &[u8]) -> Result<ImageData, String> {
317 let (pixels, width, height) = if data.starts_with(b"\x89PNG\r\n\x1a\n") {
318 let broken = |e: &dyn std::fmt::Display| format!("is a PNG that doesn't decode: {e}");
319 let mut decoder = png::Decoder::new(std::io::Cursor::new(data));
320 decoder.set_transformations(png::Transformations::normalize_to_color8());
321 let mut reader = decoder.read_info().map_err(|e| broken(&e))?;
322 let size = reader
323 .output_buffer_size()
324 .ok_or_else(|| broken(&"too big to hold"))?;
325 let mut buf = vec![0; size];
326 let info = reader.next_frame(&mut buf).map_err(|e| broken(&e))?;
327 buf.truncate(info.buffer_size());
328 let rgba = match info.color_type {
329 png::ColorType::Rgba => buf,
330 png::ColorType::Rgb => buf
331 .chunks(3)
332 .flat_map(|p| [p[0], p[1], p[2], 255])
333 .collect(),
334 png::ColorType::GrayscaleAlpha => buf
335 .chunks(2)
336 .flat_map(|p| [p[0], p[0], p[0], p[1]])
337 .collect(),
338 png::ColorType::Grayscale => buf.iter().flat_map(|&g| [g, g, g, 255]).collect(),
339 png::ColorType::Indexed => return Err(broken(&"an unexpanded palette")),
341 };
342 (rgba, info.width, info.height)
343 } else if data.starts_with(&[0xff, 0xd8, 0xff]) {
344 use zune_jpeg::JpegDecoder;
345 use zune_jpeg::zune_core::colorspace::ColorSpace;
346 use zune_jpeg::zune_core::options::DecoderOptions;
347 let broken = |e: &dyn std::fmt::Display| format!("is a JPEG that doesn't decode: {e}");
348 let options = DecoderOptions::default()
349 .set_strict_mode(true)
350 .jpeg_set_out_colorspace(ColorSpace::RGBA);
351 let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(data), options);
352 let pixels = decoder.decode().map_err(|e| broken(&e))?;
353 let info = decoder.info().ok_or_else(|| broken(&"no header"))?;
354 (pixels, info.width as u32, info.height as u32)
355 } else {
356 return Err("is not a PNG or JPEG file".into());
357 };
358 if width == 0 || height == 0 || pixels.len() != width as usize * height as usize * 4 {
359 return Err(format!(
360 "is an image of {width}×{height} pixels that doesn't decode whole"
361 ));
362 }
363 Ok(ImageData {
364 data: Blob::new(Arc::new(pixels)),
365 format: ImageFormat::Rgba8,
366 alpha_type: ImageAlphaType::Alpha,
367 width,
368 height,
369 })
370}
371
372#[derive(Debug, Clone, PartialEq)]
373pub struct Shadow {
374 pub x: f32,
375 pub y: f32,
376 pub blur: f32,
377 pub spread: f32,
378 pub color: Color,
379 pub at: Origin,
381}
382
383#[derive(Debug, Clone, PartialEq)]
384pub struct TextStyle {
385 pub font: Option<FontId>,
388 pub content: String,
389 pub size: f32,
390 pub weight: u16,
391 pub color: Color,
392 pub line_height: f32,
393 pub align: TextAlign,
394}
395
396#[derive(Debug, Clone, PartialEq)]
398pub struct Node {
399 pub id: NodeId,
402 pub kind: Kind,
403 pub name: String,
404 pub x: f32,
405 pub y: f32,
406 pub width: Size,
407 pub height: Size,
408 pub opacity: f32,
409 pub on_click: Option<String>,
410 pub fill: Option<Fill>,
411 pub stroke: Option<Color>,
412 pub stroke_width: f32,
413 pub radius: [f32; 4],
414 pub shadow: Option<Shadow>,
415 pub clip: bool,
416 pub layout: Layout,
417 pub gap: f32,
418 pub padding: [f32; 4],
419 pub align: Align,
420 pub justify: Align,
421 pub text: Option<TextStyle>,
422 pub vector: Option<Vector>,
423 pub image: Option<Image>,
424 pub children: Children<Node>,
425}
426
427impl Node {
428 pub fn same_own(&self, b: &Node) -> bool {
430 let a = self;
431 a.kind == b.kind
432 && a.name == b.name
433 && a.x == b.x
434 && a.y == b.y
435 && a.width == b.width
436 && a.height == b.height
437 && a.opacity == b.opacity
438 && a.on_click == b.on_click
439 && a.fill == b.fill
440 && a.stroke == b.stroke
441 && a.stroke_width == b.stroke_width
442 && a.radius == b.radius
443 && a.shadow == b.shadow
444 && a.clip == b.clip
445 && a.layout == b.layout
446 && a.gap == b.gap
447 && a.padding == b.padding
448 && a.align == b.align
449 && a.justify == b.justify
450 && a.text == b.text
451 && a.vector == b.vector
452 && a.image == b.image
453 }
454
455 pub fn own(&self) -> Node {
457 Node {
458 id: self.id.clone(),
459 kind: self.kind,
460 name: self.name.clone(),
461 x: self.x,
462 y: self.y,
463 width: self.width,
464 height: self.height,
465 opacity: self.opacity,
466 on_click: self.on_click.clone(),
467 fill: self.fill.clone(),
468 stroke: self.stroke,
469 stroke_width: self.stroke_width,
470 radius: self.radius,
471 shadow: self.shadow.clone(),
472 clip: self.clip,
473 layout: self.layout,
474 gap: self.gap,
475 padding: self.padding,
476 align: self.align,
477 justify: self.justify,
478 text: self.text.clone(),
479 vector: self.vector.clone(),
480 image: self.image.clone(),
481 children: Children::new(),
482 }
483 }
484
485 pub(crate) fn new(kind: Kind, name: &str) -> Node {
487 Node {
488 id: NodeId::default(),
489 kind,
490 name: name.to_string(),
491 x: 0.0,
492 y: 0.0,
493 width: Size::Hug,
494 height: Size::Hug,
495 opacity: 1.0,
496 on_click: None,
497 fill: None,
498 stroke: None,
499 stroke_width: 1.0,
500 radius: [0.0; 4],
501 shadow: None,
502 clip: false,
503 layout: Layout::None,
504 gap: 0.0,
505 padding: [0.0; 4],
506 align: Align::Start,
507 justify: Align::Start,
508 text: None,
509 vector: None,
510 image: None,
511 children: Children::new(),
512 }
513 }
514}
515
516pub trait Files {
518 fn bytes(&self, path: &str) -> Option<Vec<u8>>;
520 fn list(&self, path: &str) -> Option<Vec<String>>;
523 fn read(&self, path: &str) -> Option<String> {
525 String::from_utf8(self.bytes(path)?).ok()
526 }
527 fn parse(&self, path: &str, source: &str) -> Result<Arc<parse::File>, Problem> {
532 let _ = path;
533 parse::parse(source).map(Arc::new)
534 }
535 fn image(&self, path: &str) -> Option<Result<Arc<ImageFile>, String>> {
539 self.bytes(path)
540 .map(|bytes| ImageFile::new(bytes).map(Arc::new))
541 }
542 fn font(&self, path: &str) -> Option<Result<Arc<FontFile>, String>> {
546 self.bytes(path)
547 .map(|bytes| FontFile::new(bytes).map(Arc::new))
548 }
549}
550
551pub fn listed<'a>(paths: impl Iterator<Item = &'a String>, folder: &str) -> Option<Vec<String>> {
554 let prefix = format!("{folder}/");
555 let mut names: Vec<String> = paths
556 .filter_map(|p| p.strip_prefix(&prefix))
557 .filter(|rest| !rest.contains('/'))
558 .map(str::to_string)
559 .collect();
560 names.sort();
561 (!names.is_empty()).then_some(names)
562}
563
564impl Files for HashMap<String, String> {
565 fn bytes(&self, path: &str) -> Option<Vec<u8>> {
566 self.get(path).map(|s| s.clone().into_bytes())
567 }
568 fn list(&self, path: &str) -> Option<Vec<String>> {
569 listed(self.keys(), path)
570 }
571}
572
573#[derive(Debug, Clone)]
577pub struct Scene {
578 pub root: Node,
579 pub images: ImageFiles,
580 pub fonts: FontFiles,
581 pub themed: bool,
582 pub ids: Ids,
584}
585
586impl PartialEq for Scene {
589 fn eq(&self, other: &Scene) -> bool {
590 self.root == other.root
591 && self.images == other.images
592 && self.fonts == other.fonts
593 && self.themed == other.themed
594 }
595}
596
597impl Scene {
598 pub fn new(root: Node, images: ImageFiles, fonts: FontFiles, themed: bool) -> Scene {
600 let ids = Ids::of(&root);
601 Scene {
602 root,
603 images,
604 fonts,
605 themed,
606 ids,
607 }
608 }
609
610 pub fn node(&self, id: &NodeId) -> Option<&Node> {
612 self.at(self.ids.get(id)?)
613 }
614
615 pub fn at(&self, order: &[OrderKey]) -> Option<&Node> {
617 let mut at = &self.root;
618 for k in order {
619 at = at.children.get(k)?;
620 }
621 Some(at)
622 }
623
624 pub fn at_mut(&mut self, order: &[OrderKey]) -> Option<&mut Node> {
626 let mut at = &mut self.root;
627 for k in order {
628 at = at.children.get_mut(k)?;
629 }
630 Some(at)
631 }
632}
633
634#[derive(Clone, Debug, Default)]
637pub struct Ids(HashMap<NodeId, Order>);
638
639impl Ids {
640 pub fn of(root: &Node) -> Ids {
642 let mut ids = Ids::default();
643 ids.add(root, Vec::new());
644 ids
645 }
646
647 pub fn add(&mut self, n: &Node, order: Order) {
649 for (k, c) in n.children.entries() {
650 let mut o = order.clone();
651 o.push(k.clone());
652 self.add(c, o);
653 }
654 self.0.insert(n.id.clone(), order);
655 }
656
657 pub fn remove(&mut self, n: &Node) {
659 self.0.remove(&n.id);
660 for c in &n.children {
661 self.remove(c);
662 }
663 }
664
665 pub fn get(&self, id: &NodeId) -> Option<&Order> {
666 self.0.get(id)
667 }
668
669 pub fn contains(&self, id: &NodeId) -> bool {
670 self.0.contains_key(id)
671 }
672
673 pub fn len(&self) -> usize {
674 self.0.len()
675 }
676
677 pub fn is_empty(&self) -> bool {
678 self.0.is_empty()
679 }
680}
681
682pub type ImageFiles = std::collections::BTreeMap<ImageId, Arc<ImageFile>>;
685
686impl PartialEq for ImageFile {
688 fn eq(&self, other: &ImageFile) -> bool {
689 self.id == other.id
690 }
691}
692
693mod edit;
694#[cfg(test)]
695mod tests;
696#[cfg(test)]
697pub(crate) use edit::Bare;
698pub use edit::{Applied, Edit, Edits, Holder, Undo, Uses, applied, apply, subtree};