use crate::coord::Coord;
use crate::paint;
use crate::pixel::BlendMode;
use crate::region::Region;
use crate::stack::{Op, StackCompositor};
use alloc::vec::Vec;
pub struct Group {
pub region: Region,
pub rpn: StackCompositor,
pub hitmask: Option<Vec<u8>>,
pub text_clip: Option<Vec<u8>>,
pub blend: BlendMode,
pub id: u16,
}
impl Group {
pub fn new(region: Region, blend: BlendMode) -> Self {
let w = region.w as usize;
let h = region.h as usize;
Self {
region,
rpn: StackCompositor::new(w, h),
hitmask: None,
text_clip: None,
blend,
id: 0,
}
}
#[inline]
pub fn dims(&self) -> (usize, usize) {
(self.region.w as usize, self.region.h as usize)
}
pub fn new_layer(&mut self) -> usize {
self.rpn.new_layer()
}
pub fn set_program(&mut self, program: Vec<Op>) {
self.rpn.set_program(program);
}
pub fn enable_hitmask(&mut self) {
let (w, h) = self.dims();
self.hitmask = Some(alloc::vec![0u8; w * h]);
}
pub fn enable_text_clip(&mut self) {
let (w, h) = self.dims();
self.text_clip = Some(alloc::vec![0u8; w * h]);
}
pub fn hitmask_mut(&mut self) -> Option<&mut [u8]> {
self.hitmask.as_deref_mut()
}
pub fn text_clip_mut(&mut self) -> Option<&mut [u8]> {
self.text_clip.as_deref_mut()
}
pub fn resize(&mut self, region: Region) {
let (w, h) = (region.w as usize, region.h as usize);
self.region = region;
self.rpn.resize(w, h);
if let Some(m) = self.hitmask.as_mut() {
m.resize(w * h, 0);
m.fill(0);
}
if let Some(m) = self.text_clip.as_mut() {
m.resize(w * h, 0);
m.fill(0);
}
}
pub fn set_region(&mut self, region: Region) {
let same_dims = (region.w as usize) == (self.region.w as usize)
&& (region.h as usize) == (self.region.h as usize);
if same_dims {
self.region = region;
self.invalidate();
} else {
self.resize(region);
}
}
pub fn hit(&self, x: Coord, y: Coord) -> bool {
let Some(mask) = self.hitmask.as_ref() else {
return false;
};
if !self.region.contains(x, y) {
return false;
}
let lx = (x - self.region.x) as usize;
let ly = (y - self.region.y) as usize;
let w = self.region.w as usize;
mask[ly * w + lx] != 0
}
pub fn invalidate(&mut self) {
for layer in &mut self.rpn.layers {
layer.dirty = true;
}
}
pub fn flatten_into(
&mut self,
target: &mut [u32],
target_w: usize,
target_h: usize,
clip: Option<paint::Clip>,
) {
let composite = self.rpn.evaluate();
if composite.is_empty() {
return;
}
let (gw, gh) = (self.region.w as usize, self.region.h as usize);
let gx = self.region.x as isize;
let gy = self.region.y as isize;
let clip_rect = paint::Clip::resolve(clip, target_w, target_h);
for row in 0..gh {
let ty = gy + row as isize;
if ty < 0 || (ty as usize) >= target_h {
continue;
}
let ty_us = ty as usize;
if ty_us < clip_rect.y_start || ty_us >= clip_rect.y_end {
continue;
}
let src_row_start = row * gw;
let dst_row_start = ty_us * target_w;
let raw_start = gx.max(0) as usize;
let raw_end_isize = (gx + gw as isize).min(target_w as isize);
if raw_end_isize <= raw_start as isize {
continue;
}
let raw_end = raw_end_isize as usize;
let dst_x_start = raw_start.max(clip_rect.x_start);
let dst_x_end = raw_end.min(clip_rect.x_end);
if dst_x_end <= dst_x_start {
continue;
}
let src_clip_left = (dst_x_start as isize - gx) as usize;
let count = dst_x_end - dst_x_start;
let src_slice =
&composite[src_row_start + src_clip_left..src_row_start + src_clip_left + count];
let dst_slice = &mut target[dst_row_start + dst_x_start..dst_row_start + dst_x_end];
paint::flatten(dst_slice, src_slice, self.blend);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn opaque(visible_rgb: u32) -> u32 {
0xFF000000 | ((visible_rgb & 0x00FFFFFF) ^ 0x00FFFFFF)
}
const TRANSPARENT: u32 = 0x00000000;
#[test]
fn new_group_has_correct_bbox_buffers() {
let g = Group::new(Region::new(10.0, 20.0, 4.0, 3.0), BlendMode::Normal);
assert_eq!(g.dims(), (4, 3));
assert!(g.hitmask.is_none());
assert!(g.text_clip.is_none());
}
#[test]
fn enable_hitmask_allocates_zeroed_bbox_sized_buffer() {
let mut g = Group::new(Region::new(0.0, 0.0, 3.0, 2.0), BlendMode::Normal);
g.enable_hitmask();
let m = g.hitmask.as_ref().unwrap();
assert_eq!(m.len(), 6);
assert!(m.iter().all(|&b| b == 0));
}
#[test]
fn flatten_under_normal_blits_opaque_layer_at_offset() {
let mut g = Group::new(Region::new(1.0, 1.0, 2.0, 2.0), BlendMode::Normal);
let l = g.new_layer();
g.rpn.layers[l].pixels = alloc::vec![opaque(0xFF0000); 4];
g.set_program(alloc::vec![Op::Push(l)]);
let mut target = alloc::vec![TRANSPARENT; 4 * 4];
g.flatten_into(&mut target, 4, 4, None);
for &(x, y) in &[(1, 1), (2, 1), (1, 2), (2, 2)] {
let p = target[y * 4 + x];
let r_dark = (p >> 16) & 0xFF;
let a = p >> 24;
assert_eq!(a, 0xFF, "pixel ({x},{y}) α expected 0xFF, got {a:#x}");
assert!(
r_dark <= 0x01,
"pixel ({x},{y}) R-darkness expected ~0, got {r_dark:#x}"
);
}
assert_eq!(target[0 * 4 + 0], TRANSPARENT);
assert_eq!(target[3 * 4 + 3], TRANSPARENT);
}
#[test]
fn flatten_clips_to_target_bounds() {
let mut g = Group::new(Region::new(-1.0, -1.0, 4.0, 4.0), BlendMode::Normal);
let l = g.new_layer();
g.rpn.layers[l].pixels = alloc::vec![opaque(0x00FF00); 16];
g.set_program(alloc::vec![Op::Push(l)]);
let mut target = alloc::vec![TRANSPARENT; 4 * 4];
g.flatten_into(&mut target, 4, 4, None);
let g_pixel = target[0 * 4 + 0];
assert_eq!(
g_pixel >> 24,
0xFF,
"blitted pixel should be opaque (α=0xFF) after under-blend"
);
assert!(
((g_pixel >> 8) & 0xFF) <= 0x01,
"G-darkness ~0 (visible green)"
);
assert_eq!(target[3 * 4 + 3], TRANSPARENT);
}
#[test]
fn flatten_always_blits_for_double_buffering_safety() {
let mut g = Group::new(Region::new(0.0, 0.0, 2.0, 1.0), BlendMode::Normal);
let l = g.new_layer();
g.rpn.layers[l].pixels = alloc::vec![opaque(0xAABBCC); 2];
g.set_program(alloc::vec![Op::Push(l)]);
let mut target = alloc::vec![TRANSPARENT; 4];
g.flatten_into(&mut target, 2, 2, None);
let first = target[0];
assert_eq!(
first >> 24,
0xFF,
"first blit should make pixel opaque (α=0xFF)"
);
target.fill(TRANSPARENT);
g.flatten_into(&mut target, 2, 2, None);
let second = target[0];
assert_eq!(
second >> 24,
0xFF,
"second blit must reproduce content (double-buffer safety)"
);
assert_eq!(first, second);
}
#[test]
fn hit_returns_false_for_decorative_group() {
let g = Group::new(Region::new(0.0, 0.0, 10.0, 10.0), BlendMode::Normal);
assert!(!g.hit(5.0, 5.0));
}
#[test]
fn hit_returns_true_for_active_pixel() {
let mut g = Group::new(Region::new(2.0, 3.0, 4.0, 4.0), BlendMode::Normal);
g.enable_hitmask();
g.hitmask_mut().unwrap()[2 * 4 + 1] = 1;
assert!(g.hit(3.0, 5.0));
assert!(!g.hit(3.0, 6.0));
}
#[test]
fn hit_returns_false_outside_bbox() {
let mut g = Group::new(Region::new(2.0, 3.0, 4.0, 4.0), BlendMode::Normal);
g.enable_hitmask();
g.hitmask_mut().unwrap().fill(1);
assert!(!g.hit(1.0, 5.0)); assert!(!g.hit(7.0, 5.0)); assert!(!g.hit(3.0, 2.0)); assert!(!g.hit(3.0, 8.0)); }
}