//! Drawing a laid-out tree into a window's pixels (RGBA, premultiplied): //! the theme decides every colour and shape. use tiny_skia::{Color, FillRule, Paint, PathBuilder, PixmapMut, Stroke, Transform}; use crate::element::{Element, Kind}; use crate::layout::Rect; use crate::text::Fonts; use crate::theme::{Rgba, TextStyle, Theme, Weight}; /// The focus ring's distance outside the button, and its width. const RING_GAP: f32 = 3.0; const RING_WIDTH: f32 = 2.0; fn paint(c: Rgba) -> Paint<'static> { let mut p = Paint::default(); p.set_color(Color::from_rgba8(c.0, c.1, c.2, c.3)); p.anti_alias = true; p } /// A rectangle with corners of radius `r` (at most half its height). fn rounded(rect: Rect, r: f32) -> Option { let r = r.min(rect.h / 2.0).min(rect.w / 2.0).max(0.0); let (x0, y0, x1, y1) = (rect.x, rect.y, rect.x + rect.w, rect.y + rect.h); let mut p = PathBuilder::new(); p.move_to(x0 + r, y0); p.line_to(x1 - r, y0); p.quad_to(x1, y0, x1, y0 + r); p.line_to(x1, y1 - r); p.quad_to(x1, y1, x1 - r, y1); p.line_to(x0 + r, y1); p.quad_to(x0, y1, x0, y1 - r); p.line_to(x0, y0 + r); p.quad_to(x0, y0, x0 + r, y0); p.close(); p.finish() } /// What drawing needs at hand: the pixels, the fonts, the theme, and which /// element (pre-order) has focus. struct Painter<'p, 'a> { pm: &'p mut PixmapMut<'a>, fonts: &'p Fonts, theme: &'p Theme, focused: Option, } impl Painter<'_, '_> { /// Glyph coverage blended over the pixels in colour `c`, the pen from /// `x` on `baseline`. fn glyphs(&mut self, s: &str, (size, weight): (f32, Weight), x: f32, baseline: f32, c: Rgba) { let (pw, ph) = (self.pm.width() as i32, self.pm.height() as i32); let data = self.pm.data_mut(); self.fonts.glyphs(s, size, weight, |pen, g| { let m = &g.metrics; let gx = libm::roundf(x + pen) as i32 + m.xmin; let gy = libm::roundf(baseline) as i32 - m.height as i32 - m.ymin; for row in 0..m.height as i32 { let y = gy + row; if !(0..ph).contains(&y) { continue; } for col in 0..m.width as i32 { let x = gx + col; if !(0..pw).contains(&x) { continue; } let cov = g.coverage[(row * m.width as i32 + col) as usize] as u32 * c.3 as u32 / 255; if cov == 0 { continue; } let at = ((y * pw + x) * 4) as usize; let px = &mut data[at..at + 4]; let inv = 255 - cov; for (i, src) in [c.0, c.1, c.2].into_iter().enumerate() { px[i] = ((src as u32 * cov + px[i] as u32 * inv) / 255) as u8; } px[3] = (cov + px[3] as u32 * inv / 255) as u8; } } }); } /// Text `s` centred vertically in `r`, from its left (or centred /// across it too). fn text_in(&mut self, s: &str, style: TextStyle, r: Rect, centre: bool, c: Rgba) { let (size, weight) = self.theme.text(style); let (w, h, ascent) = self.fonts.line(s, size, weight); let x = if centre { r.x + (r.w - w) / 2.0 } else { r.x }; let baseline = r.y + (r.h - h) / 2.0 + ascent; self.glyphs(s, (size, weight), x, baseline, c); } fn draw(&mut self, e: &Element, rects: &[Rect], index: &mut usize) { let me = *index; *index += 1; let Some(&r) = rects.get(me) else { return }; let theme = self.theme; match &e.kind { Kind::Flex(f) => { for c in &f.children { self.draw(c, rects, index); } } Kind::Text(t) => self.text_in(&t.content, t.style, r, false, theme.text_colour(t.colour)), Kind::Button(b) => { let (fill, label) = theme.button(b.variant); if let Some(fill) = fill { self.fill(r, theme.corner_radius, fill); } self.text_in(&b.label, TextStyle::Label, r, true, label); } Kind::Item(i) => { self.fill(r, theme.corner_radius, theme.surface); let inner = self.inset(r); match &i.subtitle { Some(sub) => { let half = Rect { h: inner.h / 2.0, ..inner }; self.text_in(&i.title, TextStyle::Label, half, false, theme.text_primary); self.text_in(sub, TextStyle::Caption, Rect { y: inner.y + inner.h / 2.0, ..half }, false, theme.text_secondary); } None => self.text_in(&i.title, TextStyle::Label, inner, false, theme.text_primary), } // A chevron: there is more behind it. let chevron = Rect { x: inner.x + inner.w - 12.0, w: 12.0, ..inner }; self.text_in(">", TextStyle::Label, chevron, true, theme.text_secondary); } Kind::Toggle(g) => { self.fill(r, theme.corner_radius, theme.surface); let inner = self.inset(r); self.text_in(&g.label, TextStyle::Body, inner, false, theme.text_primary); let (tw, th) = crate::layout::SWITCH; let track = Rect { x: inner.x + inner.w - tw, y: inner.y + (inner.h - th) / 2.0, w: tw, h: th }; self.fill(track, th / 2.0, if g.on { theme.accent } else { theme.surface_raised }); let knob = th - 6.0; let kx = if g.on { track.x + tw - knob - 3.0 } else { track.x + 3.0 }; self.fill(Rect { x: kx, y: track.y + 3.0, w: knob, h: knob }, knob / 2.0, theme.text_primary); } } if self.focused == Some(me) && e.kind.event().is_some() { let ring = Rect { x: r.x - RING_GAP, y: r.y - RING_GAP, w: r.w + 2.0 * RING_GAP, h: r.h + 2.0 * RING_GAP }; if let Some(path) = rounded(ring, theme.corner_radius + RING_GAP) { let stroke = Stroke { width: RING_WIDTH, ..Stroke::default() }; self.pm.stroke_path(&path, &paint(theme.focus), &stroke, Transform::identity(), None); } } } /// `r` filled with `c`, its corners rounded by `radius`. fn fill(&mut self, r: Rect, radius: f32, c: Rgba) { if let Some(path) = rounded(r, radius) { self.pm.fill_path(&path, &paint(c), FillRule::Winding, Transform::identity(), None); } } /// `r` less a list row's padding all round. fn inset(&self, r: Rect) -> Rect { let p = crate::layout::row_padding(self.theme); Rect { x: r.x + p, y: r.y + p, w: (r.w - 2.0 * p).max(0.0), h: (r.h - 2.0 * p).max(0.0) } } } /// Draws `root`, laid out as `rects`, with `focused` (a pre-order index) /// ringed. pub fn render(pm: &mut PixmapMut, root: &Element, rects: &[Rect], fonts: &Fonts, theme: &Theme, focused: Option) { let bg = theme.background; pm.fill(Color::from_rgba8(bg.0, bg.1, bg.2, bg.3)); Painter { pm, fonts, theme, focused }.draw(root, rects, &mut 0); }