Files
k-os/crates/k-ui/src/render.rs

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);
}