feat(k-ui): first slice: App trait + run loop, builders (column/row/text/button), flex layout, theme tokens (Whimsy Dark), tiny-skia + fontdue render (Noto Sans Latin subset), keyboard focus ring; tally demo; manifest memory key (pm spawns with it)
This commit is contained in:
131
crates/k-ui/src/render.rs
Normal file
131
crates/k-ui/src/render.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
//! 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), Some(path)) = (fill, rounded(r, theme.corner_radius)) {
|
||||
self.pm.fill_path(&path, &paint(fill), FillRule::Winding, Transform::identity(), None);
|
||||
}
|
||||
self.text_in(&b.label, TextStyle::Label, r, true, label);
|
||||
if self.focused == Some(me) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user