171 lines
7.2 KiB
Rust
171 lines
7.2 KiB
Rust
//! 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<tiny_skia::Path> {
|
|
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<usize>,
|
|
}
|
|
|
|
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<E>(&mut self, e: &Element<E>, 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<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, focused: Option<usize>) {
|
|
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);
|
|
}
|