#[inline]
pub fn pack_argb(r: u8, g: u8, b: u8, a: u8) -> u32 {
((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
}
#[inline]
pub fn unpack_argb(packed: u32) -> (u8, u8, u8, u8) {
let a = (packed >> 24) as u8;
let r = (packed >> 16) as u8;
let g = (packed >> 8) as u8;
let b = packed as u8;
(r, g, b, a)
}
#[inline]
pub fn blend(bg: u32, fg: u32) -> u32 {
let alpha = ((fg >> 24) & 0xFF) as u64;
let inv_alpha = 256 - alpha;
let mut bg64 = bg as u64;
bg64 = (bg64 | (bg64 << 16)) & 0x0000_FFFF_0000_FFFF;
bg64 = (bg64 | (bg64 << 8)) & 0x00FF_00FF_00FF_00FF;
let mut fg64 = fg as u64;
fg64 = (fg64 | (fg64 << 16)) & 0x0000_FFFF_0000_FFFF;
fg64 = (fg64 | (fg64 << 8)) & 0x00FF_00FF_00FF_00FF;
let mut blended = bg64 * inv_alpha + fg64 * alpha;
blended = (blended >> 8) & 0x00FF_00FF_00FF_00FF;
blended = (blended | (blended >> 8)) & 0x0000_FFFF_0000_FFFF;
blended = blended | (blended >> 16);
blended as u32
}
#[inline]
fn clip_rect(buf_w: usize, buf_h: usize, x: isize, y: isize, rect_w: isize, rect_h: isize) -> (usize, usize, usize, usize) {
let x_min = if x < 0 { 0 } else if (x as usize) > buf_w { buf_w } else { x as usize };
let y_min = if y < 0 { 0 } else if (y as usize) > buf_h { buf_h } else { y as usize };
let x_end = x.saturating_add(rect_w);
let y_end = y.saturating_add(rect_h);
let x_max = if x_end < 0 { 0 } else if (x_end as usize) > buf_w { buf_w } else { x_end as usize };
let y_max = if y_end < 0 { 0 } else if (y_end as usize) > buf_h { buf_h } else { y_end as usize };
(x_min, y_min, x_max, y_max)
}
pub fn fill_rect_solid(pixels: &mut [u32], buf_w: usize, buf_h: usize, x: isize, y: isize, rect_w: isize, rect_h: isize, color: u32) {
let (x_min, y_min, x_max, y_max) = clip_rect(buf_w, buf_h, x, y, rect_w, rect_h);
for row in y_min..y_max {
let base = row * buf_w;
for col in x_min..x_max {
pixels[base + col] = color;
}
}
}
pub fn fill_rect_blend(pixels: &mut [u32], buf_w: usize, buf_h: usize, x: isize, y: isize, rect_w: isize, rect_h: isize, color: u32) {
let (x_min, y_min, x_max, y_max) = clip_rect(buf_w, buf_h, x, y, rect_w, rect_h);
for row in y_min..y_max {
let base = row * buf_w;
for col in x_min..x_max {
let idx = base + col;
pixels[idx] = blend(pixels[idx], color);
}
}
}
pub fn stroke_rect(pixels: &mut [u32], buf_w: usize, buf_h: usize, x: isize, y: isize, rect_w: isize, rect_h: isize, stroke: isize, color: u32) {
if stroke <= 0 || rect_w <= 0 || rect_h <= 0 { return; }
let solid = (color >> 24) == 0xFF;
let inner_h = rect_h - 2 * stroke;
let edges: [(isize, isize, isize, isize); 4] = [
(x, y, rect_w, stroke), (x, y + rect_h - stroke, rect_w, stroke), (x, y + stroke, stroke, inner_h), (x + rect_w - stroke, y + stroke, stroke, inner_h), ];
for &(ex, ey, ew, eh) in &edges {
if solid {
fill_rect_solid(pixels, buf_w, buf_h, ex, ey, ew, eh, color);
} else {
fill_rect_blend(pixels, buf_w, buf_h, ex, ey, ew, eh, color);
}
}
}
pub fn background_noise(pixels: &mut [u32], buf_w: usize, buf_h: usize, speckle: usize, fullscreen: bool, scroll_offset: isize) {
if buf_w < 2 || buf_h < 2 { return; }
let (row_start, row_end, x_start, x_end) = if fullscreen {
(0, buf_h, 0, buf_w)
} else {
(1, buf_h - 1, 1, buf_w - 1)
};
for row_idx in row_start..row_end {
let logical_row = row_idx as isize - scroll_offset;
let row_pixels = &mut pixels[row_idx * buf_w..(row_idx + 1) * buf_w];
background_row(row_pixels, buf_w, logical_row, buf_h, x_start, x_end, speckle);
}
}
#[inline]
fn background_row(row_pixels: &mut [u32], width: usize, logical_row: isize, height: usize, x_start: usize, x_end: usize, speckle: usize) {
use crate::theme::{BG_ALPHA, BG_BASE, BG_MASK, BG_SPECKLE};
let mut rng: usize = (0xDEAD_BEEF_0123_4567)
^ ((logical_row as usize).wrapping_sub(height / 2).wrapping_mul(0x9E37_79B9_4517_B397));
let ones = 0x0001_0101u32;
let mut colour = rng as u32 & BG_MASK | BG_ALPHA;
for x in (width / 2)..x_end {
rng ^= rng.rotate_left(13).wrapping_add(12_345_678_942);
let adder = rng as u32 & ones;
if rng.wrapping_add(speckle) < usize::MAX / 256 {
colour = (rng as u32 >> 8) & BG_SPECKLE | BG_ALPHA;
} else {
colour = colour.wrapping_add(adder) & BG_MASK;
let subtractor = (rng >> 5) as u32 & ones;
colour = colour.wrapping_sub(subtractor) & BG_MASK;
}
row_pixels[x] = colour.wrapping_add(BG_BASE) | BG_ALPHA;
}
rng = 0xDEAD_BEEF_0123_4567
^ ((logical_row as usize).wrapping_sub(height / 2).wrapping_mul(0x9E37_79B9_4517_B397));
colour = rng as u32 & BG_MASK | BG_ALPHA;
for x in (x_start..(width / 2)).rev() {
rng ^= rng.rotate_left(13).wrapping_sub(12_345_678_942);
let adder = rng as u32 & ones;
if rng.wrapping_add(speckle) < usize::MAX / 256 {
colour = (rng as u32 >> 8) & BG_SPECKLE | BG_ALPHA;
} else {
colour = colour.wrapping_add(adder) & BG_MASK;
let subtractor = (rng >> 5) as u32 & ones;
colour = colour.wrapping_sub(subtractor) & BG_MASK;
}
row_pixels[x] = colour.wrapping_add(BG_BASE) | BG_ALPHA;
}
}
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
pub const PREMULTIPLIED: bool = true;
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
pub const PREMULTIPLIED: bool = false;
pub fn scale_alpha(colour: u32, alpha: u8) -> u32 {
let mut c = colour as u64;
c = (c | (c << 16)) & 0x0000FFFF0000FFFF;
c = (c | (c << 8)) & 0x00FF00FF00FF00FF;
let mut scaled = c * alpha as u64;
scaled = (scaled >> 8) & 0x00FF00FF00FF00FF;
scaled = (scaled | (scaled >> 8)) & 0x0000FFFF0000FFFF;
scaled = scaled | (scaled >> 16);
scaled as u32
}
pub fn blend_rgb_only(bg_colour: u32, fg_colour: u32, weight_bg: u8, weight_fg: u8) -> u32 {
let mut bg = bg_colour as u64;
bg = (bg | (bg << 16)) & 0x0000FFFF0000FFFF;
bg = (bg | (bg << 8)) & 0x00FF00FF00FF00FF;
let mut fg = fg_colour as u64;
fg = (fg | (fg << 16)) & 0x0000FFFF0000FFFF;
fg = (fg | (fg << 8)) & 0x00FF00FF00FF00FF;
let mut blended = bg * weight_bg as u64 + fg * weight_fg as u64;
blended = (blended >> 8) & 0x00FF00FF00FF00FF;
blended = (blended | (blended >> 8)) & 0x0000FFFF0000FFFF;
blended = blended | (blended >> 16) | 0xFF000000;
blended as u32
}
pub mod glyph {
pub fn minimize_symbol(pixels: &mut [u32], width: usize, x: usize, y: usize, r: usize, stroke_colour: u32) {
let r = r + 1;
let r_render = r / 4 + 1;
let r_2 = r_render * r_render;
let r_4 = r_2 * r_2;
let r_3 = r_render * r_render * r_render;
let stroke_packed = stroke_colour | 0xFF00_0000;
for h in -(r_render as isize)..=(r_render as isize) {
for w in -(r as isize)..=(r as isize) {
let h2 = h * h;
let h4 = h2 * h2;
let a = (w.abs() - (r * 3 / 4) as isize).max(0);
let w2 = a * a;
let w4 = w2 * w2;
let dist_4 = (h4 + w4) as usize;
if dist_4 <= r_4 {
let px = (x as isize + w) as usize;
let py = (y as isize + h + (r / 2) as isize) as usize;
let idx = py * width + px;
let gradient = ((r_4 - dist_4) << 8) / (r_3 << 2);
if gradient > 255 {
pixels[idx] = stroke_packed;
} else {
pixels[idx] = blend_swar(pixels[idx], stroke_packed, gradient as u64);
}
}
}
}
}
pub fn maximize_symbol(pixels: &mut [u32], width: usize, x: usize, y: usize, r: usize, stroke_colour: u32, fill_colour: u32) {
let r = r + 1;
let mut r_4 = r * r;
r_4 *= r_4;
let r_3 = r * r * r;
let r_inner = r * 4 / 5;
let mut r_inner_4 = r_inner * r_inner;
r_inner_4 *= r_inner_4;
let r_inner_3 = r_inner * r_inner * r_inner;
let outer_edge_threshold = r_3 << 2;
let inner_edge_threshold = r_inner_3 << 2;
let stroke_packed = stroke_colour | 0xFF00_0000;
let fill_packed = fill_colour | 0xFF00_0000;
for h in -(r as isize)..=r as isize {
for w in -(r as isize)..=r as isize {
let h2 = h * h;
let h4 = h2 * h2;
let w2 = w * w;
let w4 = w2 * w2;
let dist_4 = (h4 + w4) as usize;
if dist_4 > r_4 { continue; }
let px = (x as isize + w) as usize;
let py = (y as isize + h) as usize;
let idx = py * width + px;
let dist_from_outer = r_4 - dist_4;
if dist_4 <= r_inner_4 {
let dist_from_inner = r_inner_4 - dist_4;
if dist_from_inner <= inner_edge_threshold {
let gradient = (dist_from_inner << 8) / inner_edge_threshold;
pixels[idx] = blend_swar(stroke_packed, fill_packed, gradient as u64);
} else {
pixels[idx] = fill_packed;
}
} else if dist_from_outer <= outer_edge_threshold {
let gradient = (dist_from_outer << 8) / outer_edge_threshold;
pixels[idx] = blend_swar(pixels[idx], stroke_packed, gradient as u64);
} else {
pixels[idx] = stroke_packed;
}
}
}
}
pub fn close_symbol(pixels: &mut [u32], width: usize, x: usize, y: usize, r: usize, stroke_colour: u32) {
let r = r + 1;
let thickness = (r / 3).max(1) as f32;
let radius = thickness / 2.0;
let size = (r * 2) as f32;
let cxf = x as f32;
let cyf = y as f32;
let end = size / 3.0;
let x1_start = cxf - end; let y1_start = cyf - end;
let x1_end = cxf + end; let y1_end = cyf + end;
let x2_start = cxf + end; let y2_start = cyf - end;
let x2_end = cxf - end; let y2_end = cyf + end;
let stroke_packed = stroke_colour | 0xFF00_0000;
let cxi = x as i32;
let cyi = y as i32;
let height = (pixels.len() / width) as i32;
let min_x = ((x as i32) - (r as i32)).max(0);
let max_x = ((x as i32) + (r as i32)).min(width as i32);
let min_y = ((y as i32) - (r as i32)).max(0);
let max_y = ((y as i32) + (r as i32)).min(height);
let quadrants: [(i32, i32, i32, i32, f32, f32, f32, f32); 4] = [
(min_x, cxi, min_y, cyi, x1_start, y1_start, x1_end, y1_end), (cxi, max_x, min_y, cyi, x2_start, y2_start, x2_end, y2_end), (min_x, cxi, cyi, max_y, x2_start, y2_start, x2_end, y2_end), (cxi, max_x, cyi, max_y, x1_start, y1_start, x1_end, y1_end), ];
for (qx0, qx1, qy0, qy1, x0, y0, x1, y1) in quadrants {
for py in qy0..qy1 {
for px in qx0..qx1 {
let dist = distance_to_capsule(
px as f32 + 0.5, py as f32 + 0.5,
x0, y0, x1, y1, radius,
);
let alpha_f = if dist < -0.5 { 1.0 } else if dist < 0.5 { 0.5 - dist } else { 0.0 };
if alpha_f > 0.0 {
let idx = py as usize * width + px as usize;
let alpha = (alpha_f * 256.0) as u64;
pixels[idx] = blend_swar(pixels[idx], stroke_packed, alpha);
}
}
}
}
}
#[inline]
fn distance_to_capsule(px: f32, py: f32, x1: f32, y1: f32, x2: f32, y2: f32, radius: f32) -> f32 {
let dx = x2 - x1;
let dy = y2 - y1;
let len_sq = dx * dx + dy * dy;
let t = ((px - x1) * dx + (py - y1) * dy) / len_sq;
let t_clamped = t.clamp(0.0, 1.0);
let cx = x1 + t_clamped * dx;
let cy = y1 + t_clamped * dy;
let ex = px - cx;
let ey = py - cy;
(ex * ex + ey * ey).sqrt() - radius
}
#[inline]
fn blend_swar(bg: u32, fg: u32, alpha: u64) -> u32 {
let inv = 256 - alpha;
let mut bg64 = bg as u64;
bg64 = (bg64 | (bg64 << 16)) & 0x0000_FFFF_0000_FFFF;
bg64 = (bg64 | (bg64 << 8)) & 0x00FF_00FF_00FF_00FF;
let mut fg64 = fg as u64;
fg64 = (fg64 | (fg64 << 16)) & 0x0000_FFFF_0000_FFFF;
fg64 = (fg64 | (fg64 << 8)) & 0x00FF_00FF_00FF_00FF;
let mut blended = bg64 * inv + fg64 * alpha;
blended = (blended >> 8) & 0x00FF_00FF_00FF_00FF;
blended = (blended | (blended >> 8)) & 0x0000_FFFF_0000_FFFF;
blended = blended | (blended >> 16);
blended as u32
}
}
pub fn circle_filled(pixels: &mut [u32], buf_w: usize, buf_h: usize, cx: isize, cy: isize, radius: isize, color: u32) {
if radius <= 0 { return; }
let r_outer = radius;
let r_outer2 = r_outer * r_outer;
let r_inner = radius - 1;
let r_inner2 = r_inner * r_inner;
let edge_range = r_outer2 - r_inner2;
let (x_min, y_min, x_max, y_max) = clip_rect(
buf_w, buf_h,
cx - r_outer, cy - r_outer,
2 * r_outer + 1, 2 * r_outer + 1,
);
let fg_alpha = (color >> 24) & 0xFF;
let color_rgb = color & 0x00FF_FFFF;
for py in y_min..y_max {
let dy = py as isize - cy;
let dy2 = dy * dy;
let base = py * buf_w;
for px in x_min..x_max {
let dx = px as isize - cx;
let dist2 = dx * dx + dy2;
if dist2 > r_outer2 { continue; }
let coverage: u32 = if dist2 <= r_inner2 {
256
} else {
(((r_outer2 - dist2) << 8) / edge_range) as u32
};
let scaled_alpha = (fg_alpha * coverage) >> 8; let scaled_color = color_rgb | (scaled_alpha << 24);
let idx = base + px;
pixels[idx] = blend(pixels[idx], scaled_color);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_unpack_round_trip() {
let cases = [(0, 0, 0, 0), (255, 255, 255, 255), (12, 34, 56, 78), (200, 100, 50, 200)];
for &(r, g, b, a) in &cases {
let p = pack_argb(r, g, b, a);
assert_eq!(unpack_argb(p), (r, g, b, a));
}
}
#[test]
fn pack_layout_is_argb() {
assert_eq!(pack_argb(0xAB, 0xCD, 0xEF, 0x12), 0x12AB_CDEF);
}
#[test]
fn blend_alpha_zero_preserves_bg() {
let bg = pack_argb(100, 150, 200, 255);
let fg = pack_argb(255, 0, 0, 0);
let result = blend(bg, fg);
let (r, g, b, _) = unpack_argb(result);
assert_eq!((r, g, b), (100, 150, 200));
}
#[test]
fn blend_alpha_full_replaces_rgb() {
let bg = pack_argb(100, 150, 200, 255);
let fg = pack_argb(50, 80, 110, 255);
let result = blend(bg, fg);
let (r, g, b, _) = unpack_argb(result);
assert!((r as i32 - 50).abs() <= 1, "r got {}", r);
assert!((g as i32 - 80).abs() <= 1, "g got {}", g);
assert!((b as i32 - 110).abs() <= 1, "b got {}", b);
}
#[test]
fn stroke_rect_only_touches_edges() {
let mut buf = vec![0u32; 10 * 10];
stroke_rect(&mut buf, 10, 10, 2, 2, 6, 6, 1, pack_argb(255, 0, 0, 255));
assert_eq!(buf[5 * 10 + 5], 0);
let (r, _, _, _) = unpack_argb(buf[2 * 10 + 2]);
assert!(r > 240, "top-left stroke pixel r={}", r);
assert_eq!(buf[1 * 10 + 1], 0);
}
#[test]
fn circle_filled_center_is_color() {
let mut buf = vec![0u32; 16 * 16];
circle_filled(&mut buf, 16, 16, 8, 8, 5, pack_argb(255, 0, 0, 255));
let (r, g, b, _) = unpack_argb(buf[8 * 16 + 8]);
assert!(r > 240 && g < 16 && b < 16, "center = ({}, {}, {})", r, g, b);
assert_eq!(buf[0], 0);
}
#[test]
fn circle_filled_clips_partial_offscreen() {
let mut buf = vec![0u32; 8 * 8];
circle_filled(&mut buf, 8, 8, -2, -2, 4, pack_argb(255, 255, 255, 255));
let (r, _, _, _) = unpack_argb(buf[0]);
assert!(r > 200, "buf[0] r={}", r);
}
#[test]
fn blend_alpha_half_is_midpoint() {
let bg = pack_argb(0, 0, 0, 255);
let fg = pack_argb(200, 200, 200, 128);
let result = blend(bg, fg);
let (r, g, b, _) = unpack_argb(result);
assert!((r as i32 - 100).abs() <= 1, "r got {}", r);
assert!((g as i32 - 100).abs() <= 1, "g got {}", g);
assert!((b as i32 - 100).abs() <= 1, "b got {}", b);
}
#[test]
fn fill_rect_solid_full_buffer() {
let mut buf = vec![0u32; 4 * 4];
fill_rect_solid(&mut buf, 4, 4, 0, 0, 4, 4, 0xFF112233);
assert!(buf.iter().all(|&p| p == 0xFF112233));
}
#[test]
fn fill_rect_solid_partial() {
let mut buf = vec![0u32; 4 * 4];
fill_rect_solid(&mut buf, 4, 4, 1, 1, 2, 2, 0xFFAABBCC);
let expected: [u32; 16] = [
0, 0, 0, 0,
0, 0xFFAABBCC, 0xFFAABBCC, 0,
0, 0xFFAABBCC, 0xFFAABBCC, 0,
0, 0, 0, 0,
];
assert_eq!(buf.as_slice(), &expected);
}
#[test]
fn fill_rect_solid_clips_negative_origin() {
let mut buf = vec![0u32; 4 * 4];
fill_rect_solid(&mut buf, 4, 4, -2, -2, 4, 4, 0xFF000001);
let expected: [u32; 16] = [
0xFF000001, 0xFF000001, 0, 0,
0xFF000001, 0xFF000001, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
];
assert_eq!(buf.as_slice(), &expected);
}
#[test]
fn fill_rect_solid_fully_offscreen_is_noop() {
let mut buf = vec![0u32; 4 * 4];
fill_rect_solid(&mut buf, 4, 4, 100, 100, 5, 5, 0xFFFFFFFF);
assert!(buf.iter().all(|&p| p == 0));
fill_rect_solid(&mut buf, 4, 4, -10, -10, 5, 5, 0xFFFFFFFF);
assert!(buf.iter().all(|&p| p == 0));
}
#[test]
fn fill_rect_blend_alpha_zero_no_change() {
let mut buf = vec![pack_argb(50, 60, 70, 255); 4 * 4];
fill_rect_blend(&mut buf, 4, 4, 0, 0, 4, 4, pack_argb(255, 0, 0, 0));
assert!(buf.iter().all(|&p| {
let (r, g, b, _) = unpack_argb(p);
(r, g, b) == (50, 60, 70)
}));
}
#[test]
fn fill_rect_blend_clips_partial() {
let mut buf = vec![pack_argb(0, 0, 0, 255); 4 * 4];
fill_rect_blend(&mut buf, 4, 4, 2, 2, 10, 10, pack_argb(200, 200, 200, 128));
for y in 0..4usize {
for x in 0..4usize {
let (r, g, b, _) = unpack_argb(buf[y * 4 + x]);
if x >= 2 && y >= 2 {
assert!((r as i32 - 100).abs() <= 1);
assert!((g as i32 - 100).abs() <= 1);
assert!((b as i32 - 100).abs() <= 1);
} else {
assert_eq!((r, g, b), (0, 0, 0));
}
}
}
}
}