use std::collections::BTreeMap;
use azul_core::{
dom::{DomId, NodeId},
geom::{LogicalPosition, LogicalRect, LogicalSize},
resources::RendererResources,
};
#[derive(Debug, Clone)]
pub struct HeadlessConfig {
pub width: f32,
pub height: f32,
pub dpi_factor: f32,
pub enable_rendering: bool,
pub max_iterations: Option<usize>,
}
const DEFAULT_MAX_ITERATIONS: usize = 1000;
impl Default for HeadlessConfig {
fn default() -> Self {
Self {
width: 800.0,
height: 600.0,
dpi_factor: 1.0,
enable_rendering: false,
max_iterations: Some(DEFAULT_MAX_ITERATIONS),
}
}
}
impl HeadlessConfig {
pub fn new() -> Self {
Self::default()
}
}
pub struct CpuHitTester {
node_rects: BTreeMap<DomId, Vec<HitTestEntry>>,
}
#[derive(Debug, Clone)]
struct HitTestEntry {
node_id: NodeId,
rect: LogicalRect,
clip: Option<LogicalRect>,
pointer_events_none: bool,
}
impl CpuHitTester {
pub fn new() -> Self {
Self {
node_rects: BTreeMap::new(),
}
}
pub fn node_rects_total(&self) -> usize {
self.node_rects.values().map(|v| v.len()).sum()
}
pub fn rebuild_from_layout(
&mut self,
layout_results: &BTreeMap<DomId, crate::window::DomLayoutResult>,
) {
self.node_rects.clear();
for (dom_id, layout_result) in layout_results {
let mut entries = Vec::new();
let positions = &layout_result.calculated_positions;
let nodes = &layout_result.layout_tree.nodes;
for (idx, node) in nodes.iter().enumerate() {
let node_id = match node.dom_node_id {
Some(id) => id,
None => continue, };
let pos = match positions.get(idx) {
Some(p) => *p,
None => continue,
};
let size = match node.used_size {
Some(s) => s,
None => continue,
};
let rect = LogicalRect {
origin: pos,
size,
};
entries.push(HitTestEntry {
node_id,
rect,
clip: None, pointer_events_none: false, });
}
self.node_rects.insert(*dom_id, entries);
}
}
pub fn hit_test(
&self,
position: LogicalPosition,
) -> Vec<(DomId, NodeId)> {
let mut results = Vec::new();
for (dom_id, entries) in &self.node_rects {
for entry in entries.iter().rev() {
if entry.pointer_events_none {
continue;
}
if let Some(ref clip) = entry.clip {
if !point_in_rect(position, clip) {
continue;
}
}
if point_in_rect(position, &entry.rect) {
results.push((*dom_id, entry.node_id));
}
}
}
results
}
}
fn point_in_rect(point: LogicalPosition, rect: &LogicalRect) -> bool {
point.x >= rect.origin.x
&& point.x < rect.origin.x + rect.size.width
&& point.y >= rect.origin.y
&& point.y < rect.origin.y + rect.size.height
}
#[cfg(feature = "cpurender")]
pub struct HeadlessRenderer {
pub width: f32,
pub height: f32,
pub dpi_factor: f32,
}
#[cfg(feature = "cpurender")]
impl HeadlessRenderer {
pub fn new(width: f32, height: f32, dpi_factor: f32) -> Self {
Self {
width,
height,
dpi_factor,
}
}
pub fn render_frame(
&self,
display_list: &crate::solver3::display_list::DisplayList,
renderer_resources: &RendererResources,
) -> Result<crate::cpurender::AzulPixmap, String> {
let mut glyph_cache = crate::glyph_cache::GlyphCache::new();
crate::cpurender::render(
display_list,
renderer_resources,
crate::cpurender::RenderOptions {
width: self.width,
height: self.height,
dpi_factor: self.dpi_factor,
},
&mut glyph_cache,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_headless_config_default() {
let config = HeadlessConfig::default();
assert_eq!(config.width, 800.0);
assert_eq!(config.height, 600.0);
assert_eq!(config.dpi_factor, 1.0);
assert!(!config.enable_rendering);
assert_eq!(config.max_iterations, Some(DEFAULT_MAX_ITERATIONS));
}
#[test]
fn test_cpu_hit_tester_empty() {
let tester = CpuHitTester::new();
let results = tester.hit_test(LogicalPosition { x: 100.0, y: 100.0 });
assert!(results.is_empty());
}
#[test]
fn test_point_in_rect() {
let rect = LogicalRect {
origin: LogicalPosition { x: 10.0, y: 10.0 },
size: LogicalSize {
width: 100.0,
height: 50.0,
},
};
assert!(point_in_rect(LogicalPosition { x: 50.0, y: 30.0 }, &rect));
assert!(point_in_rect(LogicalPosition { x: 10.0, y: 10.0 }, &rect));
assert!(!point_in_rect(LogicalPosition { x: 5.0, y: 5.0 }, &rect));
assert!(!point_in_rect(LogicalPosition { x: 200.0, y: 30.0 }, &rect));
}
}