use std::cmp::Ordering;
use std::collections::HashMap;
use crate::accessibility::AccessibilityPreferences;
use crate::host::HostNodeInteraction;
use crate::platform::{
BackendCapabilities, BackendCapabilityRequirement, CursorGrabMode, CursorRequest,
InputCapabilityKind, LayerOrder, LogicalRect, PixelSize, PlatformRequest,
PlatformRequestIdAllocator, PlatformResponse, PlatformServiceCapabilities,
PlatformServiceCapabilityKind, PlatformServiceRequest, PlatformServiceResponse, ResourceHandle,
ResourceId, ResourceKind,
};
use crate::{
AccessibilityMeta, AccessibilityRole, AccessibilitySummary, CanvasContent, ColorRgba,
DirtyFlags, FrameTiming, PaintImage, PaintItem, PaintKind, PaintList, PaintTransform,
ShaderEffect, UiNodeId, UiPoint, UiRect, UiSize,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResourceFormat {
Rgba8,
Bgra8,
Alpha8,
}
impl ResourceFormat {
pub const fn bytes_per_pixel(self) -> usize {
match self {
Self::Rgba8 | Self::Bgra8 => 4,
Self::Alpha8 => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PixelRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl PixelRect {
pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub const fn is_empty(self) -> bool {
self.width == 0 || self.height == 0
}
pub const fn right(self) -> u32 {
self.x.saturating_add(self.width)
}
pub const fn bottom(self) -> u32 {
self.y.saturating_add(self.height)
}
pub const fn contains(self, size: PixelSize) -> bool {
self.right() <= size.width && self.bottom() <= size.height
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResourceDescriptor {
pub handle: ResourceHandle,
pub size: PixelSize,
pub format: ResourceFormat,
pub version: u64,
}
impl ResourceDescriptor {
pub fn new(handle: ResourceHandle, size: PixelSize, format: ResourceFormat) -> Self {
Self {
handle,
size,
format,
version: 0,
}
}
pub fn version(mut self, version: u64) -> Self {
self.version = version;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResourceUpdate {
pub descriptor: ResourceDescriptor,
pub dirty_rect: Option<PixelRect>,
pub bytes: Vec<u8>,
}
impl ResourceUpdate {
pub fn full(descriptor: ResourceDescriptor, bytes: Vec<u8>) -> Self {
Self {
descriptor,
dirty_rect: None,
bytes,
}
}
pub fn partial(descriptor: ResourceDescriptor, dirty_rect: PixelRect, bytes: Vec<u8>) -> Self {
Self {
descriptor,
dirty_rect: Some(dirty_rect),
bytes,
}
}
pub fn is_partial(&self) -> bool {
self.dirty_rect.is_some()
}
pub fn expected_byte_len(&self) -> Option<usize> {
let pixels = match self.dirty_rect {
Some(rect) => usize::try_from(rect.width)
.ok()?
.checked_mul(usize::try_from(rect.height).ok()?)?,
None => usize::try_from(self.descriptor.size.width)
.ok()?
.checked_mul(usize::try_from(self.descriptor.size.height).ok()?)?,
};
pixels.checked_mul(self.descriptor.format.bytes_per_pixel())
}
pub fn has_expected_byte_len(&self) -> bool {
self.expected_byte_len()
.is_some_and(|expected| expected == self.bytes.len())
}
pub fn dirty_rect_is_valid(&self) -> bool {
self.dirty_rect
.map(|rect| !rect.is_empty() && rect.contains(self.descriptor.size))
.unwrap_or(true)
}
}
pub trait ResourceResolver {
fn resolve_resource(&self, id: &ResourceId) -> Option<ResourceDescriptor>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RenderTargetKind {
Window,
Offscreen,
Snapshot,
AppOwned,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RenderTarget {
Window {
id: String,
size: UiSize,
},
Offscreen {
label: Option<String>,
size: PixelSize,
},
Snapshot {
label: Option<String>,
size: PixelSize,
},
AppOwned {
id: String,
size: UiSize,
},
}
impl RenderTarget {
pub fn window(id: impl Into<String>, size: UiSize) -> Self {
Self::Window {
id: id.into(),
size,
}
}
pub fn offscreen(size: PixelSize) -> Self {
Self::Offscreen { label: None, size }
}
pub fn snapshot(size: PixelSize) -> Self {
Self::Snapshot { label: None, size }
}
pub fn app_owned(id: impl Into<String>, size: UiSize) -> Self {
Self::AppOwned {
id: id.into(),
size,
}
}
pub const fn kind(&self) -> RenderTargetKind {
match self {
Self::Window { .. } => RenderTargetKind::Window,
Self::Offscreen { .. } => RenderTargetKind::Offscreen,
Self::Snapshot { .. } => RenderTargetKind::Snapshot,
Self::AppOwned { .. } => RenderTargetKind::AppOwned,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DirtyRegionSet {
pub regions: Vec<UiRect>,
}
impl DirtyRegionSet {
pub fn empty() -> Self {
Self {
regions: Vec::new(),
}
}
pub fn full(viewport: UiSize) -> Self {
Self {
regions: vec![UiRect::new(0.0, 0.0, viewport.width, viewport.height)],
}
}
pub fn push(&mut self, region: UiRect) -> bool {
if !rect_is_finite(region) || region.width <= 0.0 || region.height <= 0.0 {
return false;
}
if self.regions.contains(®ion) {
return false;
}
self.regions.push(region);
true
}
pub fn is_empty(&self) -> bool {
self.regions.is_empty()
}
pub fn covers(&self, rect: UiRect) -> bool {
self.regions.iter().any(|region| region.contains_rect(rect))
}
}
impl Default for DirtyRegionSet {
fn default() -> Self {
Self::empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RenderOptions {
pub scale_factor: f32,
pub deterministic: bool,
pub allow_partial_updates: bool,
pub collect_gpu_timing: bool,
pub clear_color: ColorRgba,
pub accessibility_preferences: AccessibilityPreferences,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
scale_factor: 1.0,
deterministic: false,
allow_partial_updates: true,
collect_gpu_timing: false,
clear_color: ColorRgba::TRANSPARENT,
accessibility_preferences: AccessibilityPreferences::DEFAULT,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RenderFrameRequest {
pub target: RenderTarget,
pub viewport: UiSize,
pub paint: PaintList,
pub dirty_regions: DirtyRegionSet,
pub resource_updates: Vec<ResourceUpdate>,
pub node_interactions: HashMap<UiNodeId, HostNodeInteraction>,
pub dirty_flags: DirtyFlags,
pub options: RenderOptions,
}
impl RenderFrameRequest {
pub fn new(target: RenderTarget, viewport: UiSize, paint: PaintList) -> Self {
Self {
target,
viewport,
paint,
dirty_regions: DirtyRegionSet::full(viewport),
resource_updates: Vec::new(),
node_interactions: HashMap::new(),
dirty_flags: DirtyFlags::ALL,
options: RenderOptions::default(),
}
}
pub fn dirty_regions(mut self, dirty_regions: DirtyRegionSet) -> Self {
self.dirty_regions = dirty_regions;
self
}
pub fn resource_update(mut self, update: ResourceUpdate) -> Self {
self.resource_updates.push(update);
self
}
pub fn node_interaction(mut self, node: UiNodeId, interaction: HostNodeInteraction) -> Self {
self.node_interactions.insert(node, interaction);
self
}
pub fn node_interactions(
mut self,
interactions: impl IntoIterator<Item = (UiNodeId, HostNodeInteraction)>,
) -> Self {
self.node_interactions.extend(interactions);
self
}
pub fn interaction_for(&self, node: UiNodeId) -> HostNodeInteraction {
self.node_interactions
.get(&node)
.copied()
.unwrap_or_default()
}
pub fn dirty_flags(mut self, dirty_flags: DirtyFlags) -> Self {
self.dirty_flags = dirty_flags;
self
}
pub fn options(mut self, options: RenderOptions) -> Self {
self.options = options;
self
}
pub fn batches(&self) -> Vec<PaintBatch> {
PaintBatcher::default().batch(&self.paint)
}
pub fn canvas_requests(&self) -> Vec<CanvasRenderRequest> {
self.paint
.items
.iter()
.filter_map(CanvasRenderRequest::from_paint_item)
.collect()
}
pub fn canvas_host_capture_plans(&self) -> Vec<CanvasHostCapturePlan> {
self.canvas_requests()
.into_iter()
.filter(|request| request.requires_host_input_capture())
.map(|request| request.host_capture_plan())
.collect()
}
pub fn canvas_platform_requests(&self) -> Vec<PlatformRequest> {
self.canvas_host_capture_plans()
.into_iter()
.flat_map(|plan| plan.platform_requests())
.collect()
}
pub fn image_requests(&self) -> Vec<ImageRenderRequest> {
self.paint
.items
.iter()
.filter_map(ImageRenderRequest::from_paint_item)
.collect()
}
pub fn requires_full_repaint(&self) -> bool {
self.dirty_regions.is_empty()
|| self.dirty_flags.layout
|| self.dirty_flags.theme
|| self.dirty_flags.text_measurement
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CanvasRenderRequest {
pub node: UiNodeId,
pub canvas: CanvasContent,
pub rect: UiRect,
pub clip_rect: UiRect,
pub z_index: i16,
pub layer_order: LayerOrder,
pub opacity: f32,
pub transform: PaintTransform,
}
impl CanvasRenderRequest {
pub fn from_paint_item(item: &PaintItem) -> Option<Self> {
let PaintKind::Canvas(canvas) = &item.kind else {
return None;
};
Some(Self {
node: item.node,
canvas: canvas.clone(),
rect: item.rect,
clip_rect: item.clip_rect,
z_index: item.z_index,
layer_order: item.layer_order,
opacity: item.opacity,
transform: item.transform,
})
}
pub const fn requires_host_input_capture(&self) -> bool {
self.canvas.requires_host_input_capture()
}
pub fn host_capture_plan(&self) -> CanvasHostCapturePlan {
CanvasHostCapturePlan::from_request(self)
}
pub fn surface_key(&self) -> &str {
self.canvas.surface_key()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CanvasHostCapturePlan {
pub node: UiNodeId,
pub key: String,
pub rect: UiRect,
pub pointer_capture: bool,
pub keyboard_capture: bool,
pub wheel_capture: bool,
pub pointer_lock: bool,
pub domain_hit_testing: bool,
}
impl CanvasHostCapturePlan {
pub fn from_request(request: &CanvasRenderRequest) -> Self {
let interaction = request.canvas.interaction;
Self {
node: request.node,
key: request.canvas.key.clone(),
rect: request.rect,
pointer_capture: interaction.pointer_capture,
keyboard_capture: interaction.keyboard_capture,
wheel_capture: interaction.wheel_capture,
pointer_lock: interaction.pointer_lock,
domain_hit_testing: interaction.domain_hit_testing,
}
}
pub const fn requires_host_capture(&self) -> bool {
self.pointer_capture || self.keyboard_capture || self.wheel_capture || self.pointer_lock
}
pub fn cursor_confine_rect(&self) -> Option<LogicalRect> {
self.pointer_lock.then(|| ui_rect_to_logical(self.rect))
}
pub fn platform_requests(&self) -> Vec<PlatformRequest> {
let Some(_rect) = self.cursor_confine_rect() else {
return Vec::new();
};
vec![
PlatformRequest::Cursor(CursorRequest::SetGrab(CursorGrabMode::Locked)),
PlatformRequest::Cursor(CursorRequest::SetVisible(false)),
]
}
pub fn release_platform_requests(&self) -> Vec<PlatformRequest> {
if self.pointer_lock {
vec![
PlatformRequest::Cursor(CursorRequest::SetGrab(CursorGrabMode::None)),
PlatformRequest::Cursor(CursorRequest::SetVisible(true)),
]
} else {
Vec::new()
}
}
pub fn capability_requirements(&self) -> Vec<BackendCapabilityRequirement> {
let mut requirements = Vec::new();
if self.pointer_capture || self.domain_hit_testing || self.pointer_lock {
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::Input(InputCapabilityKind::PointerMove),
);
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::Input(InputCapabilityKind::PointerButton),
);
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::Input(InputCapabilityKind::CanvasLocalInput),
);
}
if self.wheel_capture {
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::Input(InputCapabilityKind::PointerWheel),
);
}
if self.keyboard_capture {
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::Input(InputCapabilityKind::KeyboardPress),
);
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::Input(InputCapabilityKind::KeyboardRelease),
);
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::Input(InputCapabilityKind::Modifiers),
);
}
if self.pointer_lock {
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::Input(InputCapabilityKind::RawMouseMotion),
);
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::Input(InputCapabilityKind::PointerLock),
);
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::PlatformService(
PlatformServiceCapabilityKind::CursorGrab,
),
);
push_unique_requirement(
&mut requirements,
BackendCapabilityRequirement::PlatformService(
PlatformServiceCapabilityKind::CursorVisibility,
),
);
}
requirements
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CanvasHostCaptureId {
pub node: UiNodeId,
pub key: String,
}
impl CanvasHostCaptureId {
pub fn new(node: UiNodeId, key: impl Into<String>) -> Self {
Self {
node,
key: key.into(),
}
}
pub fn from_plan(plan: &CanvasHostCapturePlan) -> Self {
Self::new(plan.node, plan.key.clone())
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CanvasHostCaptureState {
active: Vec<CanvasHostCapturePlan>,
}
impl CanvasHostCaptureState {
pub fn new() -> Self {
Self::default()
}
pub fn active_plans(&self) -> &[CanvasHostCapturePlan] {
&self.active
}
pub fn active_plan(&self, id: &CanvasHostCaptureId) -> Option<&CanvasHostCapturePlan> {
self.active
.iter()
.find(|plan| CanvasHostCaptureId::from_plan(plan) == *id)
}
pub fn is_empty(&self) -> bool {
self.active.is_empty()
}
pub fn sync(
&mut self,
plans: impl IntoIterator<Item = CanvasHostCapturePlan>,
) -> CanvasHostCaptureTransition {
let next = normalized_capture_plans(plans);
let previous = self.active.clone();
let mut transition = CanvasHostCaptureTransition::new();
for previous_plan in &previous {
let id = CanvasHostCaptureId::from_plan(previous_plan);
match next
.iter()
.find(|plan| CanvasHostCaptureId::from_plan(plan) == id)
{
Some(next_plan) if next_plan != previous_plan => {
transition.changes.push(CanvasHostCaptureChange {
kind: CanvasHostCaptureChangeKind::Updated,
id,
previous: Some(previous_plan.clone()),
current: Some(next_plan.clone()),
});
}
None => {
transition.changes.push(CanvasHostCaptureChange {
kind: CanvasHostCaptureChangeKind::Released,
id,
previous: Some(previous_plan.clone()),
current: None,
});
}
Some(_) => {}
}
}
for next_plan in &next {
let id = CanvasHostCaptureId::from_plan(next_plan);
if previous
.iter()
.all(|plan| CanvasHostCaptureId::from_plan(plan) != id)
{
transition.changes.push(CanvasHostCaptureChange {
kind: CanvasHostCaptureChangeKind::Acquired,
id,
previous: None,
current: Some(next_plan.clone()),
});
}
}
transition.sort();
self.active = next;
transition
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CanvasHostCaptureChangeKind {
Acquired,
Updated,
Released,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CanvasHostCaptureChange {
pub kind: CanvasHostCaptureChangeKind,
pub id: CanvasHostCaptureId,
pub previous: Option<CanvasHostCapturePlan>,
pub current: Option<CanvasHostCapturePlan>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CanvasHostCaptureTransition {
pub changes: Vec<CanvasHostCaptureChange>,
}
impl CanvasHostCaptureTransition {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
pub fn platform_requests(&self) -> Vec<PlatformRequest> {
let mut requests = Vec::new();
for change in &self.changes {
if matches!(
change.kind,
CanvasHostCaptureChangeKind::Updated | CanvasHostCaptureChangeKind::Released
) {
if let Some(previous) = &change.previous {
requests.extend(previous.release_platform_requests());
}
}
}
for change in &self.changes {
if matches!(
change.kind,
CanvasHostCaptureChangeKind::Acquired | CanvasHostCaptureChangeKind::Updated
) {
if let Some(current) = &change.current {
requests.extend(current.platform_requests());
}
}
}
requests
}
pub fn platform_service_requests(
&self,
allocator: &mut PlatformRequestIdAllocator,
) -> Vec<PlatformServiceRequest> {
allocator.allocate_all(self.platform_requests())
}
fn sort(&mut self) {
self.changes.sort_by(|a, b| {
let release_order = change_order(a.kind).cmp(&change_order(b.kind));
if release_order == Ordering::Equal {
capture_id_order(&a.id, &b.id)
} else {
release_order
}
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CanvasHostCaptureDiagnosticKind {
Active,
Acquired,
Updated,
Released,
Unavailable,
Denied,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CanvasHostCaptureDiagnostic {
pub kind: CanvasHostCaptureDiagnosticKind,
pub id: Option<CanvasHostCaptureId>,
pub node: Option<UiNodeId>,
pub key: Option<String>,
pub reason: String,
pub previous: Option<CanvasHostCapturePlan>,
pub current: Option<CanvasHostCapturePlan>,
pub platform_requests: Vec<PlatformRequest>,
pub unsupported_requests: Vec<PlatformRequest>,
pub platform_responses: Vec<PlatformServiceResponse>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CanvasHostCaptureDiagnosticReport {
pub diagnostics: Vec<CanvasHostCaptureDiagnostic>,
}
impl CanvasHostCaptureDiagnosticReport {
pub fn from_state_transition(
state: &CanvasHostCaptureState,
transition: &CanvasHostCaptureTransition,
capabilities: PlatformServiceCapabilities,
) -> Self {
let mut report = Self::default();
if transition.is_empty() {
report.diagnostics.extend(
state
.active_plans()
.iter()
.cloned()
.map(active_capture_diagnostic),
);
return report;
}
report.diagnostics.extend(
transition
.changes
.iter()
.map(|change| transition_capture_diagnostic(change, capabilities)),
);
report
}
pub fn with_platform_responses(
mut self,
requests: &[PlatformServiceRequest],
responses: &[PlatformServiceResponse],
) -> Self {
for response in responses {
if !is_denied_cursor_response(response) {
continue;
}
let request = requests
.iter()
.find(|request| response.is_for(request))
.map(|request| request.request.clone());
let source = request
.as_ref()
.and_then(|request| {
self.diagnostics.iter().find(|diagnostic| {
diagnostic
.platform_requests
.iter()
.any(|candidate| candidate == request)
})
})
.map(|diagnostic| {
(
diagnostic.id.clone(),
diagnostic.node,
diagnostic.key.clone(),
diagnostic.previous.clone(),
diagnostic.current.clone(),
)
})
.unwrap_or((None, None, None, None, None));
self.diagnostics.push(CanvasHostCaptureDiagnostic {
kind: CanvasHostCaptureDiagnosticKind::Denied,
id: source.0,
node: source.1,
key: source.2,
reason: "Host denied or failed a cursor request used by canvas capture.".to_owned(),
previous: source.3,
current: source.4,
platform_requests: request.into_iter().collect(),
unsupported_requests: Vec::new(),
platform_responses: vec![response.clone()],
});
}
self
}
pub fn has_kind(&self, kind: CanvasHostCaptureDiagnosticKind) -> bool {
self.diagnostics
.iter()
.any(|diagnostic| diagnostic.kind == kind)
}
pub fn diagnostics_for(
&self,
id: &CanvasHostCaptureId,
) -> impl Iterator<Item = &CanvasHostCaptureDiagnostic> {
let id = id.clone();
self.diagnostics
.iter()
.filter(move |diagnostic| diagnostic.id.as_ref() == Some(&id))
}
}
fn normalized_capture_plans(
plans: impl IntoIterator<Item = CanvasHostCapturePlan>,
) -> Vec<CanvasHostCapturePlan> {
let mut normalized = Vec::new();
for plan in plans {
if !plan.requires_host_capture() {
continue;
}
let id = CanvasHostCaptureId::from_plan(&plan);
if let Some(existing) = normalized
.iter()
.position(|existing| CanvasHostCaptureId::from_plan(existing) == id)
{
normalized[existing] = plan;
} else {
normalized.push(plan);
}
}
normalized.sort_by(|a, b| {
capture_id_order(
&CanvasHostCaptureId::from_plan(a),
&CanvasHostCaptureId::from_plan(b),
)
});
normalized
}
fn change_order(kind: CanvasHostCaptureChangeKind) -> u8 {
match kind {
CanvasHostCaptureChangeKind::Released => 0,
CanvasHostCaptureChangeKind::Updated => 1,
CanvasHostCaptureChangeKind::Acquired => 2,
}
}
fn capture_id_order(a: &CanvasHostCaptureId, b: &CanvasHostCaptureId) -> Ordering {
a.node.0.cmp(&b.node.0).then_with(|| a.key.cmp(&b.key))
}
fn active_capture_diagnostic(plan: CanvasHostCapturePlan) -> CanvasHostCaptureDiagnostic {
let id = CanvasHostCaptureId::from_plan(&plan);
CanvasHostCaptureDiagnostic {
kind: CanvasHostCaptureDiagnosticKind::Active,
id: Some(id),
node: Some(plan.node),
key: Some(plan.key.clone()),
reason: "Canvas host capture is active and unchanged.".to_owned(),
previous: Some(plan.clone()),
current: Some(plan),
platform_requests: Vec::new(),
unsupported_requests: Vec::new(),
platform_responses: Vec::new(),
}
}
fn transition_capture_diagnostic(
change: &CanvasHostCaptureChange,
capabilities: PlatformServiceCapabilities,
) -> CanvasHostCaptureDiagnostic {
let platform_requests = capture_platform_requests_for_change(change);
let unsupported_requests = platform_requests
.iter()
.filter(|request| !capabilities.supports(request))
.cloned()
.collect::<Vec<_>>();
let kind = if unsupported_requests.is_empty() {
match change.kind {
CanvasHostCaptureChangeKind::Acquired => CanvasHostCaptureDiagnosticKind::Acquired,
CanvasHostCaptureChangeKind::Updated => CanvasHostCaptureDiagnosticKind::Updated,
CanvasHostCaptureChangeKind::Released => CanvasHostCaptureDiagnosticKind::Released,
}
} else {
CanvasHostCaptureDiagnosticKind::Unavailable
};
let plan = change.current.as_ref().or(change.previous.as_ref());
CanvasHostCaptureDiagnostic {
kind,
id: Some(change.id.clone()),
node: plan.map(|plan| plan.node),
key: plan.map(|plan| plan.key.clone()),
reason: capture_diagnostic_reason(change.kind, kind, unsupported_requests.len()),
previous: change.previous.clone(),
current: change.current.clone(),
platform_requests,
unsupported_requests,
platform_responses: Vec::new(),
}
}
fn capture_platform_requests_for_change(change: &CanvasHostCaptureChange) -> Vec<PlatformRequest> {
let mut requests = Vec::new();
if matches!(
change.kind,
CanvasHostCaptureChangeKind::Updated | CanvasHostCaptureChangeKind::Released
) {
if let Some(previous) = &change.previous {
requests.extend(previous.release_platform_requests());
}
}
if matches!(
change.kind,
CanvasHostCaptureChangeKind::Acquired | CanvasHostCaptureChangeKind::Updated
) {
if let Some(current) = &change.current {
requests.extend(current.platform_requests());
}
}
requests
}
fn capture_diagnostic_reason(
change: CanvasHostCaptureChangeKind,
kind: CanvasHostCaptureDiagnosticKind,
unsupported_count: usize,
) -> String {
if kind == CanvasHostCaptureDiagnosticKind::Unavailable {
return format!(
"Canvas host capture {:?} needs {unsupported_count} platform request(s) unsupported by current backend capabilities.",
change
);
}
match change {
CanvasHostCaptureChangeKind::Acquired => "Canvas host capture was acquired.".to_owned(),
CanvasHostCaptureChangeKind::Updated => {
"Canvas host capture requirements changed.".to_owned()
}
CanvasHostCaptureChangeKind::Released => "Canvas host capture was released.".to_owned(),
}
}
fn push_unique_requirement(
requirements: &mut Vec<BackendCapabilityRequirement>,
requirement: BackendCapabilityRequirement,
) {
if !requirements.contains(&requirement) {
requirements.push(requirement);
}
}
fn is_denied_cursor_response(response: &PlatformServiceResponse) -> bool {
matches!(
&response.response,
PlatformResponse::Cursor(crate::platform::CursorResponse::Unsupported)
| PlatformResponse::Cursor(crate::platform::CursorResponse::Error(_))
)
}
#[derive(Debug)]
pub struct CanvasRenderContext<'a, B> {
pub request: &'a CanvasRenderRequest,
pub scale_factor: f32,
pub dirty_regions: &'a DirtyRegionSet,
pub interaction: HostNodeInteraction,
pub backend: &'a mut B,
}
impl<B> CanvasRenderContext<'_, B> {
pub fn is_dirty(&self) -> bool {
self.dirty_regions.is_empty() || self.dirty_regions.covers(self.request.rect)
}
pub fn surface_size(&self) -> PixelSize {
let width = (self.request.rect.width * self.scale_factor)
.ceil()
.max(1.0);
let height = (self.request.rect.height * self.scale_factor)
.ceil()
.max(1.0);
PixelSize::new(
width.min(u32::MAX as f32) as u32,
height.min(u32::MAX as f32) as u32,
)
}
pub fn surface_descriptor(&self, format: ResourceFormat) -> ResourceDescriptor {
self.request
.canvas
.surface_descriptor(self.surface_size(), format)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CanvasHitTarget {
pub id: String,
pub rect: UiRect,
pub label: Option<String>,
pub value: Option<String>,
pub metadata: Vec<(String, String)>,
pub z_index: i16,
pub disabled: bool,
}
impl CanvasHitTarget {
pub fn new(id: impl Into<String>, rect: UiRect) -> Self {
Self {
id: id.into(),
rect,
label: None,
value: None,
metadata: Vec::new(),
z_index: 0,
disabled: false,
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.push((key.into(), value.into()));
self
}
pub const fn z_index(mut self, z_index: i16) -> Self {
self.z_index = z_index;
self
}
pub const fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn contains(&self, point: UiPoint) -> bool {
!self.disabled && self.rect.contains_point(point)
}
pub fn display_label(&self) -> &str {
self.label.as_deref().unwrap_or(self.id.as_str())
}
pub fn display_value(&self, index: usize, total: usize) -> String {
let position = format!("{} of {}", index.saturating_add(1), total);
match &self.value {
Some(value) if !value.is_empty() => format!("{value}; target {position}"),
_ => format!("target {position}"),
}
}
pub fn accessibility_summary(&self) -> AccessibilitySummary {
let mut summary = AccessibilitySummary::new(self.display_label())
.item("Target id", self.id.clone())
.item(
"Bounds",
format!(
"{:.1}, {:.1}, {:.1}, {:.1}",
self.rect.x, self.rect.y, self.rect.width, self.rect.height
),
);
if let Some(value) = &self.value {
if !value.is_empty() {
summary = summary.item("Value", value.clone());
}
}
for (key, value) in &self.metadata {
summary = summary.item(key.clone(), value.clone());
}
summary
}
pub fn accessibility_meta(
&self,
index: usize,
total: usize,
active: bool,
) -> AccessibilityMeta {
let mut meta = AccessibilityMeta::new(AccessibilityRole::ListItem)
.label(self.display_label())
.value(self.display_value(index, total))
.selected(active)
.summary(self.accessibility_summary());
if self.disabled {
meta = meta.disabled();
} else {
meta = meta.focusable();
}
meta
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CanvasHitCollection {
pub node: UiNodeId,
pub key: String,
pub targets: Vec<CanvasHitTarget>,
}
impl CanvasHitCollection {
pub fn new(node: UiNodeId, key: impl Into<String>) -> Self {
Self {
node,
key: key.into(),
targets: Vec::new(),
}
}
pub fn target(mut self, target: CanvasHitTarget) -> Self {
self.targets.push(target);
self
}
pub fn is_empty(&self) -> bool {
self.targets.is_empty()
}
pub fn len(&self) -> usize {
self.targets.len()
}
pub fn topmost_at(&self, point: UiPoint) -> Option<&CanvasHitTarget> {
self.targets
.iter()
.enumerate()
.filter(|(_, target)| target.contains(point))
.max_by(|left, right| {
left.1
.z_index
.cmp(&right.1.z_index)
.then_with(|| left.0.cmp(&right.0))
})
.map(|(_, target)| target)
}
pub fn accessibility_summary(&self, title: impl Into<String>) -> AccessibilitySummary {
let enabled = self
.targets
.iter()
.filter(|target| !target.disabled)
.count();
let labelled = self
.targets
.iter()
.filter(|target| target.label.is_some())
.count();
AccessibilitySummary::new(title)
.item("Canvas key", self.key.clone())
.item("Targets", self.targets.len().to_string())
.item("Enabled targets", enabled.to_string())
.item("Labelled targets", labelled.to_string())
}
pub fn accessibility_meta(&self, label: impl Into<String>) -> AccessibilityMeta {
let label = label.into();
let enabled = self
.targets
.iter()
.filter(|target| !target.disabled)
.count();
AccessibilityMeta::new(AccessibilityRole::List)
.label(label.clone())
.value(format!(
"{} targets; {} enabled",
self.targets.len(),
enabled
))
.summary(self.accessibility_summary(label))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CanvasRenderOutput {
pub dirty_region: Option<UiRect>,
pub resource_updates: Vec<ResourceUpdate>,
pub hit_targets: Vec<CanvasHitTarget>,
pub repaint_requested: bool,
}
impl CanvasRenderOutput {
pub fn new() -> Self {
Self {
dirty_region: None,
resource_updates: Vec::new(),
hit_targets: Vec::new(),
repaint_requested: false,
}
}
pub fn dirty_region(mut self, dirty_region: UiRect) -> Self {
self.dirty_region = Some(dirty_region);
self
}
pub fn resource_update(mut self, update: ResourceUpdate) -> Self {
self.resource_updates.push(update);
self
}
pub fn hit_target(mut self, target: CanvasHitTarget) -> Self {
self.hit_targets.push(target);
self
}
pub fn hit_targets(mut self, targets: impl IntoIterator<Item = CanvasHitTarget>) -> Self {
self.hit_targets.extend(targets);
self
}
pub fn repaint_requested(mut self, repaint_requested: bool) -> Self {
self.repaint_requested = repaint_requested;
self
}
}
impl Default for CanvasRenderOutput {
fn default() -> Self {
Self::new()
}
}
pub trait CanvasRenderHandler<B> {
fn render_canvas(
&mut self,
context: CanvasRenderContext<'_, B>,
) -> Result<CanvasRenderOutput, RenderError>;
}
impl<B, F> CanvasRenderHandler<B> for F
where
F: for<'a> FnMut(CanvasRenderContext<'a, B>) -> Result<CanvasRenderOutput, RenderError>,
{
fn render_canvas(
&mut self,
context: CanvasRenderContext<'_, B>,
) -> Result<CanvasRenderOutput, RenderError> {
self(context)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CanvasRenderOutcome {
Rendered {
request: CanvasRenderRequest,
output: CanvasRenderOutput,
},
Missing {
request: CanvasRenderRequest,
},
Failed {
request: CanvasRenderRequest,
error: RenderError,
},
}
impl CanvasRenderOutcome {
pub const fn request(&self) -> &CanvasRenderRequest {
match self {
Self::Rendered { request, .. }
| Self::Missing { request }
| Self::Failed { request, .. } => request,
}
}
pub const fn is_rendered(&self) -> bool {
matches!(self, Self::Rendered { .. })
}
pub const fn is_missing(&self) -> bool {
matches!(self, Self::Missing { .. })
}
pub const fn is_failed(&self) -> bool {
matches!(self, Self::Failed { .. })
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CanvasRenderReport {
pub outcomes: Vec<CanvasRenderOutcome>,
}
impl CanvasRenderReport {
pub fn rendered_count(&self) -> usize {
self.outcomes
.iter()
.filter(|outcome| outcome.is_rendered())
.count()
}
pub fn missing_count(&self) -> usize {
self.outcomes
.iter()
.filter(|outcome| outcome.is_missing())
.count()
}
pub fn failed_count(&self) -> usize {
self.outcomes
.iter()
.filter(|outcome| outcome.is_failed())
.count()
}
pub fn repaint_requested(&self) -> bool {
self.outcomes.iter().any(|outcome| {
matches!(
outcome,
CanvasRenderOutcome::Rendered {
output: CanvasRenderOutput {
repaint_requested: true,
..
},
..
}
)
})
}
pub fn first_failure(&self) -> Option<&RenderError> {
self.outcomes.iter().find_map(|outcome| match outcome {
CanvasRenderOutcome::Failed { error, .. } => Some(error),
_ => None,
})
}
pub fn first_missing(&self) -> Option<&CanvasRenderRequest> {
self.outcomes.iter().find_map(|outcome| match outcome {
CanvasRenderOutcome::Missing { request } => Some(request),
_ => None,
})
}
pub fn resource_updates(&self) -> Vec<ResourceUpdate> {
let mut updates = Vec::new();
for outcome in &self.outcomes {
if let CanvasRenderOutcome::Rendered { output, .. } = outcome {
updates.extend(output.resource_updates.iter().cloned());
}
}
updates
}
pub fn hit_collections(&self) -> Vec<CanvasHitCollection> {
let mut collections = Vec::new();
for outcome in &self.outcomes {
if let CanvasRenderOutcome::Rendered { request, output } = outcome {
if output.hit_targets.is_empty() {
continue;
}
collections.push(CanvasHitCollection {
node: request.node,
key: request.canvas.key.clone(),
targets: output.hit_targets.clone(),
});
}
}
collections
}
pub fn hit_targets(&self) -> Vec<CanvasHitTarget> {
self.hit_collections()
.into_iter()
.flat_map(|collection| collection.targets)
.collect()
}
pub fn into_strict_result(self) -> Result<Self, RenderError> {
if let Some(error) = self.first_failure().cloned() {
return Err(error);
}
if let Some(missing) = self.first_missing() {
return Err(RenderError::MissingCanvasRenderer(
missing.canvas.key.clone(),
));
}
Ok(self)
}
}
pub struct CanvasRenderRegistry<B> {
handlers: HashMap<String, Box<dyn CanvasRenderHandler<B>>>,
}
impl<B> CanvasRenderRegistry<B> {
pub fn new() -> Self {
Self {
handlers: HashMap::new(),
}
}
pub fn register(
&mut self,
key: impl Into<String>,
handler: impl CanvasRenderHandler<B> + 'static,
) -> bool {
self.handlers
.insert(key.into(), Box::new(handler))
.is_some()
}
pub fn unregister(&mut self, key: &str) -> bool {
self.handlers.remove(key).is_some()
}
pub fn contains(&self, key: &str) -> bool {
self.handlers.contains_key(key)
}
pub fn len(&self) -> usize {
self.handlers.len()
}
pub fn is_empty(&self) -> bool {
self.handlers.is_empty()
}
pub fn render_frame_canvases(
&mut self,
request: &RenderFrameRequest,
backend: &mut B,
) -> CanvasRenderReport {
let mut report = CanvasRenderReport::default();
for canvas_request in request.canvas_requests() {
let Some(handler) = self.handlers.get_mut(&canvas_request.canvas.key) else {
report.outcomes.push(CanvasRenderOutcome::Missing {
request: canvas_request,
});
continue;
};
let outcome = match handler.render_canvas(CanvasRenderContext {
scale_factor: request.options.scale_factor,
dirty_regions: &request.dirty_regions,
interaction: request.interaction_for(canvas_request.node),
request: &canvas_request,
backend,
}) {
Ok(output) => CanvasRenderOutcome::Rendered {
request: canvas_request,
output,
},
Err(error) => CanvasRenderOutcome::Failed {
request: canvas_request,
error,
},
};
report.outcomes.push(outcome);
}
report
}
pub fn render_frame_canvases_strict(
&mut self,
request: &RenderFrameRequest,
backend: &mut B,
) -> Result<CanvasRenderReport, RenderError> {
self.render_frame_canvases(request, backend)
.into_strict_result()
}
}
impl<B> Default for CanvasRenderRegistry<B> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ImageRenderKind {
NodeImage,
ImagePlacement,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImageRenderRequest {
pub node: UiNodeId,
pub kind: ImageRenderKind,
pub image: PaintImage,
pub clip_rect: UiRect,
pub z_index: i16,
pub layer_order: LayerOrder,
pub opacity: f32,
pub transform: PaintTransform,
}
impl ImageRenderRequest {
pub fn from_paint_item(item: &PaintItem) -> Option<Self> {
let (kind, image) = match &item.kind {
PaintKind::Image { key, tint } => {
let mut image = PaintImage::new(key.clone(), item.rect);
if let Some(tint) = *tint {
image = image.tinted(tint);
}
(ImageRenderKind::NodeImage, image)
}
PaintKind::ImagePlacement(image) => (ImageRenderKind::ImagePlacement, image.clone()),
_ => return None,
};
Some(Self {
node: item.node,
kind,
image,
clip_rect: item.clip_rect,
z_index: item.z_index,
layer_order: item.layer_order,
opacity: item.opacity,
transform: item.transform,
})
}
pub fn key(&self) -> &str {
&self.image.key
}
}
#[derive(Debug)]
pub struct ImageRenderContext<'a, B> {
pub request: &'a ImageRenderRequest,
pub scale_factor: f32,
pub dirty_regions: &'a DirtyRegionSet,
pub interaction: HostNodeInteraction,
pub backend: &'a mut B,
}
impl<B> ImageRenderContext<'_, B> {
pub fn is_dirty(&self) -> bool {
self.dirty_regions.is_empty() || self.dirty_regions.covers(self.request.image.rect)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImageRenderOutput {
pub dirty_region: Option<UiRect>,
pub resource_updates: Vec<ResourceUpdate>,
pub repaint_requested: bool,
}
impl ImageRenderOutput {
pub fn new() -> Self {
Self {
dirty_region: None,
resource_updates: Vec::new(),
repaint_requested: false,
}
}
pub fn dirty_region(mut self, dirty_region: UiRect) -> Self {
self.dirty_region = Some(dirty_region);
self
}
pub fn resource_update(mut self, update: ResourceUpdate) -> Self {
self.resource_updates.push(update);
self
}
pub fn repaint_requested(mut self, repaint_requested: bool) -> Self {
self.repaint_requested = repaint_requested;
self
}
}
impl Default for ImageRenderOutput {
fn default() -> Self {
Self::new()
}
}
pub trait ImageRenderHandler<B> {
fn render_image(
&mut self,
context: ImageRenderContext<'_, B>,
) -> Result<ImageRenderOutput, RenderError>;
}
impl<B, F> ImageRenderHandler<B> for F
where
F: for<'a> FnMut(ImageRenderContext<'a, B>) -> Result<ImageRenderOutput, RenderError>,
{
fn render_image(
&mut self,
context: ImageRenderContext<'_, B>,
) -> Result<ImageRenderOutput, RenderError> {
self(context)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ImageRenderOutcome {
Rendered {
request: ImageRenderRequest,
output: ImageRenderOutput,
},
Missing {
request: ImageRenderRequest,
},
Failed {
request: ImageRenderRequest,
error: RenderError,
},
}
impl ImageRenderOutcome {
pub const fn request(&self) -> &ImageRenderRequest {
match self {
Self::Rendered { request, .. }
| Self::Missing { request }
| Self::Failed { request, .. } => request,
}
}
pub const fn is_rendered(&self) -> bool {
matches!(self, Self::Rendered { .. })
}
pub const fn is_missing(&self) -> bool {
matches!(self, Self::Missing { .. })
}
pub const fn is_failed(&self) -> bool {
matches!(self, Self::Failed { .. })
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ImageRenderReport {
pub outcomes: Vec<ImageRenderOutcome>,
}
impl ImageRenderReport {
pub fn rendered_count(&self) -> usize {
self.outcomes
.iter()
.filter(|outcome| outcome.is_rendered())
.count()
}
pub fn missing_count(&self) -> usize {
self.outcomes
.iter()
.filter(|outcome| outcome.is_missing())
.count()
}
pub fn failed_count(&self) -> usize {
self.outcomes
.iter()
.filter(|outcome| outcome.is_failed())
.count()
}
pub fn repaint_requested(&self) -> bool {
self.outcomes.iter().any(|outcome| {
matches!(
outcome,
ImageRenderOutcome::Rendered {
output: ImageRenderOutput {
repaint_requested: true,
..
},
..
}
)
})
}
pub fn first_failure(&self) -> Option<&RenderError> {
self.outcomes.iter().find_map(|outcome| match outcome {
ImageRenderOutcome::Failed { error, .. } => Some(error),
_ => None,
})
}
pub fn first_missing(&self) -> Option<&ImageRenderRequest> {
self.outcomes.iter().find_map(|outcome| match outcome {
ImageRenderOutcome::Missing { request } => Some(request),
_ => None,
})
}
pub fn resource_updates(&self) -> Vec<ResourceUpdate> {
let mut updates = Vec::new();
for outcome in &self.outcomes {
if let ImageRenderOutcome::Rendered { output, .. } = outcome {
updates.extend(output.resource_updates.iter().cloned());
}
}
updates
}
pub fn into_strict_result(self) -> Result<Self, RenderError> {
if let Some(error) = self.first_failure().cloned() {
return Err(error);
}
if let Some(missing) = self.first_missing() {
return Err(RenderError::MissingImageRenderer(missing.image.key.clone()));
}
Ok(self)
}
}
pub struct ImageRenderRegistry<B> {
handlers: HashMap<String, Box<dyn ImageRenderHandler<B>>>,
}
impl<B> ImageRenderRegistry<B> {
pub fn new() -> Self {
Self {
handlers: HashMap::new(),
}
}
pub fn register(
&mut self,
key: impl Into<String>,
handler: impl ImageRenderHandler<B> + 'static,
) -> bool {
self.handlers
.insert(key.into(), Box::new(handler))
.is_some()
}
pub fn unregister(&mut self, key: &str) -> bool {
self.handlers.remove(key).is_some()
}
pub fn contains(&self, key: &str) -> bool {
self.handlers.contains_key(key)
}
pub fn len(&self) -> usize {
self.handlers.len()
}
pub fn is_empty(&self) -> bool {
self.handlers.is_empty()
}
pub fn render_frame_images(
&mut self,
request: &RenderFrameRequest,
backend: &mut B,
) -> ImageRenderReport {
let mut report = ImageRenderReport::default();
for image_request in request.image_requests() {
let Some(handler) = self.handlers.get_mut(&image_request.image.key) else {
report.outcomes.push(ImageRenderOutcome::Missing {
request: image_request,
});
continue;
};
let outcome = match handler.render_image(ImageRenderContext {
scale_factor: request.options.scale_factor,
dirty_regions: &request.dirty_regions,
interaction: request.interaction_for(image_request.node),
request: &image_request,
backend,
}) {
Ok(output) => ImageRenderOutcome::Rendered {
request: image_request,
output,
},
Err(error) => ImageRenderOutcome::Failed {
request: image_request,
error,
},
};
report.outcomes.push(outcome);
}
report
}
pub fn render_frame_images_strict(
&mut self,
request: &RenderFrameRequest,
backend: &mut B,
) -> Result<ImageRenderReport, RenderError> {
self.render_frame_images(request, backend)
.into_strict_result()
}
}
impl<B> Default for ImageRenderRegistry<B> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PaintBatchKind {
Rect,
Text,
Canvas,
Line,
Circle,
Polygon,
Image,
CompositedLayer,
RichRect,
SceneText,
Path,
ImagePlacement,
}
impl PaintBatchKind {
pub const fn from_kind(kind: &PaintKind) -> Self {
match kind {
PaintKind::Rect { .. } => Self::Rect,
PaintKind::Text(_) => Self::Text,
PaintKind::Canvas(_) => Self::Canvas,
PaintKind::Line { .. } => Self::Line,
PaintKind::Circle { .. } => Self::Circle,
PaintKind::Polygon { .. } => Self::Polygon,
PaintKind::Image { .. } => Self::Image,
PaintKind::CompositedLayer(_) => Self::CompositedLayer,
PaintKind::RichRect(_) => Self::RichRect,
PaintKind::SceneText(_) => Self::SceneText,
PaintKind::Path(_) => Self::Path,
PaintKind::ImagePlacement(_) => Self::ImagePlacement,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PaintBatchKey {
pub kind: PaintBatchKind,
pub z_index: i16,
pub layer_order: LayerOrder,
pub clip_rect: UiRect,
pub shader: Option<ShaderEffect>,
}
impl PaintBatchKey {
pub fn from_item(item: &PaintItem) -> Self {
Self {
kind: PaintBatchKind::from_kind(&item.kind),
z_index: item.z_index,
layer_order: item.layer_order,
clip_rect: item.clip_rect,
shader: item.shader.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PaintBatch {
pub key: PaintBatchKey,
pub item_indices: Vec<usize>,
pub bounds: UiRect,
}
impl PaintBatch {
fn new(index: usize, item: &PaintItem) -> Self {
Self {
key: PaintBatchKey::from_item(item),
item_indices: vec![index],
bounds: item.rect,
}
}
fn try_push(&mut self, index: usize, item: &PaintItem) -> bool {
if self.key != PaintBatchKey::from_item(item) {
return false;
}
self.item_indices.push(index);
self.bounds = union_rect(self.bounds, item.rect);
true
}
pub fn len(&self) -> usize {
self.item_indices.len()
}
pub fn is_empty(&self) -> bool {
self.item_indices.is_empty()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PaintBatcher {
pub preserve_order: bool,
}
impl PaintBatcher {
pub fn batch(self, paint: &PaintList) -> Vec<PaintBatch> {
let mut batches = Vec::<PaintBatch>::new();
for (index, item) in paint.items.iter().enumerate() {
if self.preserve_order
|| !batches
.last_mut()
.is_some_and(|batch| batch.try_push(index, item))
{
batches.push(PaintBatch::new(index, item));
}
}
batches
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenderError {
UnsupportedTarget(RenderTargetKind),
UnsupportedResource(ResourceKind),
MissingResource(ResourceId),
MissingCanvasRenderer(String),
MissingImageRenderer(String),
InvalidResourceUpdate(String),
Backend(String),
}
impl std::fmt::Display for RenderError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnsupportedTarget(target) => {
write!(formatter, "unsupported render target {target:?}")
}
Self::UnsupportedResource(resource) => {
write!(formatter, "unsupported render resource {resource:?}")
}
Self::MissingResource(resource) => {
write!(formatter, "missing render resource {:?}", resource.key)
}
Self::MissingCanvasRenderer(key) => {
write!(formatter, "missing canvas renderer for {key:?}")
}
Self::MissingImageRenderer(key) => {
write!(formatter, "missing image renderer for {key:?}")
}
Self::InvalidResourceUpdate(reason) => {
write!(formatter, "invalid render resource update: {reason}")
}
Self::Backend(reason) => formatter.write_str(reason),
}
}
}
impl std::error::Error for RenderError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderedImage {
pub size: PixelSize,
pub format: ResourceFormat,
pub pixels: Vec<u8>,
}
impl RenderedImage {
pub fn new(size: PixelSize, format: ResourceFormat, pixels: Vec<u8>) -> Self {
Self {
size,
format,
pixels,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RenderFrameOutput {
pub target: RenderTarget,
pub painted_items: usize,
pub batches: Vec<PaintBatch>,
pub dirty_regions: DirtyRegionSet,
pub timings: FrameTiming,
pub snapshot: Option<RenderedImage>,
}
impl RenderFrameOutput {
pub fn new(target: RenderTarget) -> Self {
Self {
target,
painted_items: 0,
batches: Vec::new(),
dirty_regions: DirtyRegionSet::default(),
timings: FrameTiming::default(),
snapshot: None,
}
}
}
pub trait RendererAdapter {
fn capabilities(&self) -> BackendCapabilities;
fn render_frame(
&mut self,
request: RenderFrameRequest,
resolver: &dyn ResourceResolver,
) -> Result<RenderFrameOutput, RenderError>;
}
fn rect_is_finite(rect: UiRect) -> bool {
rect.x.is_finite() && rect.y.is_finite() && rect.width.is_finite() && rect.height.is_finite()
}
fn ui_rect_to_logical(rect: UiRect) -> LogicalRect {
LogicalRect::new(rect.x, rect.y, rect.width, rect.height)
}
fn union_rect(a: UiRect, b: UiRect) -> UiRect {
let left = a.x.min(b.x);
let top = a.y.min(b.y);
let right = a.right().max(b.right());
let bottom = a.bottom().max(b.bottom());
UiRect::new(left, top, right - left, bottom - top)
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::platform::{
BackendAdapterKind, ImageHandle, PlatformRequestId, RenderingCapabilities,
ResourceCapabilities, ResourceDomain,
};
use crate::{
CanvasContent, CanvasInteractionPolicy, CanvasRenderMode, ImageAlignment, ImageFit,
PaintImage, PaintTransform, ShaderEffect, StrokeStyle, TextContent, TextStyle, UiNodeId,
};
fn paint_item(index: usize, rect: UiRect, kind: PaintKind) -> PaintItem {
PaintItem {
node: UiNodeId(index),
rect,
clip_rect: UiRect::new(0.0, 0.0, 200.0, 100.0),
z_index: 0,
layer_order: LayerOrder::DEFAULT,
opacity: 1.0,
transform: PaintTransform::default(),
shader: None,
kind,
}
}
#[test]
fn render_options_default_to_neutral_accessibility_preferences() {
assert_eq!(
RenderOptions::default().accessibility_preferences,
AccessibilityPreferences::DEFAULT
);
}
#[test]
fn resource_updates_validate_full_and_partial_texture_deltas() {
let descriptor = ResourceDescriptor::new(
ResourceHandle::Image(ImageHandle::app("menu.thumbnail")),
PixelSize::new(4, 4),
ResourceFormat::Rgba8,
)
.version(7);
let full = ResourceUpdate::full(descriptor.clone(), vec![0; 4 * 4 * 4]);
assert!(!full.is_partial());
assert_eq!(full.expected_byte_len(), Some(64));
assert!(full.has_expected_byte_len());
assert!(full.dirty_rect_is_valid());
let partial = ResourceUpdate::partial(
descriptor.clone(),
PixelRect::new(1, 1, 2, 2),
vec![255; 2 * 2 * 4],
);
assert!(partial.is_partial());
assert_eq!(partial.expected_byte_len(), Some(16));
assert!(partial.has_expected_byte_len());
assert!(partial.dirty_rect_is_valid());
let invalid =
ResourceUpdate::partial(descriptor, PixelRect::new(3, 3, 2, 2), vec![0; 2 * 2 * 4]);
assert!(!invalid.dirty_rect_is_valid());
}
#[test]
fn paint_batcher_groups_contiguous_items_by_kind_clip_z_and_shader() {
let rect = PaintKind::Rect {
fill: ColorRgba::new(20, 20, 20, 255),
stroke: Some(StrokeStyle::new(ColorRgba::new(90, 90, 90, 255), 1.0)),
corner_radius: 4.0,
};
let mut paint = PaintList::default();
paint.items.push(paint_item(
0,
UiRect::new(0.0, 0.0, 20.0, 20.0),
rect.clone(),
));
paint.items.push(paint_item(
1,
UiRect::new(16.0, 0.0, 20.0, 20.0),
rect.clone(),
));
let mut debug_overlay_item = paint_item(3, UiRect::new(24.0, 0.0, 20.0, 20.0), rect);
debug_overlay_item.layer_order = LayerOrder::new(crate::platform::UiLayer::DebugOverlay, 0);
paint.items.push(debug_overlay_item);
let mut shader_item = paint_item(
2,
UiRect::new(40.0, 0.0, 20.0, 20.0),
PaintKind::Text(TextContent::new("A", TextStyle::default())),
);
shader_item.shader = Some(ShaderEffect::new("text.glow"));
paint.items.push(shader_item);
let batches = PaintBatcher::default().batch(&paint);
assert_eq!(batches.len(), 3);
assert_eq!(batches[0].key.kind, PaintBatchKind::Rect);
assert_eq!(batches[0].key.layer_order, LayerOrder::DEFAULT);
assert_eq!(batches[0].item_indices, vec![0, 1]);
assert_eq!(batches[0].bounds, UiRect::new(0.0, 0.0, 36.0, 20.0));
assert_eq!(
batches[1].key.layer_order,
LayerOrder::new(crate::platform::UiLayer::DebugOverlay, 0)
);
assert_eq!(batches[2].key.kind, PaintBatchKind::Text);
assert_eq!(batches[2].key.shader.as_ref().unwrap().key, "text.glow");
let unbatched = PaintBatcher {
preserve_order: true,
}
.batch(&paint);
assert_eq!(unbatched.len(), 4);
}
#[test]
fn render_request_tracks_dirty_regions_batches_and_full_repaint_policy() {
let mut paint = PaintList::default();
paint.items.push(paint_item(
0,
UiRect::new(10.0, 10.0, 20.0, 20.0),
PaintKind::Image {
key: "icons.play".to_string(),
tint: None,
},
));
let mut dirty = DirtyRegionSet::empty();
assert!(dirty.push(UiRect::new(8.0, 8.0, 24.0, 24.0)));
let request = RenderFrameRequest::new(
RenderTarget::snapshot(PixelSize::new(128, 64)),
UiSize::new(128.0, 64.0),
paint,
)
.dirty_regions(dirty.clone())
.dirty_flags(DirtyFlags {
paint: true,
..DirtyFlags::NONE
});
assert!(!request.requires_full_repaint());
assert!(request
.dirty_regions
.covers(UiRect::new(10.0, 10.0, 20.0, 20.0)));
assert_eq!(request.batches().len(), 1);
let full = request.clone().dirty_flags(DirtyFlags {
layout: true,
..DirtyFlags::NONE
});
assert!(full.requires_full_repaint());
}
#[test]
fn render_request_extracts_embedded_canvas_requests() {
let canvas = CanvasContent::new("editor.mask.viewport")
.native_viewport()
.interaction(CanvasInteractionPolicy::NATIVE_VIEWPORT);
let mut paint = PaintList::default();
paint.items.push(paint_item(
7,
UiRect::new(12.0, 16.0, 320.0, 180.0),
PaintKind::Canvas(canvas),
));
let request = RenderFrameRequest::new(
RenderTarget::app_owned("main", UiSize::new(640.0, 480.0)),
UiSize::new(640.0, 480.0),
paint,
);
let canvases = request.canvas_requests();
assert_eq!(canvases.len(), 1);
assert_eq!(canvases[0].node, UiNodeId(7));
assert_eq!(canvases[0].canvas.key, "editor.mask.viewport");
assert_eq!(
canvases[0].canvas.render_mode,
CanvasRenderMode::NativeViewport
);
assert!(canvases[0].requires_host_input_capture());
assert!(canvases[0].canvas.interaction.pointer_lock);
assert!(canvases[0].canvas.interaction.domain_hit_testing);
assert_eq!(canvases[0].rect, UiRect::new(12.0, 16.0, 320.0, 180.0));
}
#[test]
fn canvas_host_capture_plan_maps_pointer_lock_to_cursor_requests() {
let canvas = CanvasContent::new("editor.mask.viewport")
.native_viewport()
.interaction(CanvasInteractionPolicy::NATIVE_VIEWPORT);
let mut paint = PaintList::default();
paint.items.push(paint_item(
7,
UiRect::new(12.0, 16.0, 320.0, 180.0),
PaintKind::Canvas(canvas),
));
let request = RenderFrameRequest::new(
RenderTarget::app_owned("main", UiSize::new(640.0, 480.0)),
UiSize::new(640.0, 480.0),
paint,
);
let plans = request.canvas_host_capture_plans();
assert_eq!(plans.len(), 1);
assert_eq!(plans[0].node, UiNodeId(7));
assert_eq!(plans[0].key, "editor.mask.viewport");
assert_eq!(plans[0].rect, UiRect::new(12.0, 16.0, 320.0, 180.0));
assert!(plans[0].pointer_capture);
assert!(plans[0].keyboard_capture);
assert!(plans[0].wheel_capture);
assert!(plans[0].pointer_lock);
assert!(plans[0].domain_hit_testing);
assert!(plans[0].requires_host_capture());
assert_eq!(
plans[0].cursor_confine_rect(),
Some(LogicalRect::new(12.0, 16.0, 320.0, 180.0))
);
assert_eq!(
request.canvas_platform_requests(),
vec![
PlatformRequest::Cursor(CursorRequest::SetGrab(CursorGrabMode::Locked)),
PlatformRequest::Cursor(CursorRequest::SetVisible(false)),
]
);
assert_eq!(
plans[0].release_platform_requests(),
vec![
PlatformRequest::Cursor(CursorRequest::SetGrab(CursorGrabMode::None)),
PlatformRequest::Cursor(CursorRequest::SetVisible(true)),
]
);
let requirements = plans[0].capability_requirements();
assert!(requirements.contains(&BackendCapabilityRequirement::Input(
InputCapabilityKind::KeyboardRelease
)));
assert!(requirements.contains(&BackendCapabilityRequirement::Input(
InputCapabilityKind::RawMouseMotion
)));
assert!(requirements.contains(&BackendCapabilityRequirement::Input(
InputCapabilityKind::PointerLock
)));
assert!(
requirements.contains(&BackendCapabilityRequirement::PlatformService(
PlatformServiceCapabilityKind::CursorGrab
))
);
assert!(
requirements.contains(&BackendCapabilityRequirement::PlatformService(
PlatformServiceCapabilityKind::CursorVisibility
))
);
}
#[test]
fn canvas_host_capture_plan_keeps_editor_capture_without_pointer_lock_requests() {
let canvas = CanvasContent::new("orbifold.curve.editor")
.interaction(CanvasInteractionPolicy::EDITOR);
let mut paint = PaintList::default();
paint.items.push(paint_item(
11,
UiRect::new(24.0, 32.0, 400.0, 240.0),
PaintKind::Canvas(canvas),
));
let request = RenderFrameRequest::new(
RenderTarget::window("main", UiSize::new(800.0, 600.0)),
UiSize::new(800.0, 600.0),
paint,
);
let plans = request.canvas_host_capture_plans();
assert_eq!(plans.len(), 1);
assert_eq!(plans[0].node, UiNodeId(11));
assert_eq!(plans[0].key, "orbifold.curve.editor");
assert!(plans[0].pointer_capture);
assert!(plans[0].keyboard_capture);
assert!(plans[0].wheel_capture);
assert!(!plans[0].pointer_lock);
assert!(plans[0].domain_hit_testing);
assert_eq!(plans[0].cursor_confine_rect(), None);
assert!(plans[0].platform_requests().is_empty());
assert!(plans[0].release_platform_requests().is_empty());
assert!(request.canvas_platform_requests().is_empty());
let requirements = plans[0].capability_requirements();
assert!(requirements.contains(&BackendCapabilityRequirement::Input(
InputCapabilityKind::KeyboardRelease
)));
assert!(requirements.contains(&BackendCapabilityRequirement::Input(
InputCapabilityKind::PointerWheel
)));
assert!(!requirements.contains(&BackendCapabilityRequirement::Input(
InputCapabilityKind::RawMouseMotion
)));
assert!(
!requirements.contains(&BackendCapabilityRequirement::PlatformService(
PlatformServiceCapabilityKind::CursorGrab
))
);
}
fn capture_plan(node: usize, key: &str, rect: UiRect) -> CanvasHostCapturePlan {
CanvasHostCapturePlan {
node: UiNodeId(node),
key: key.to_string(),
rect,
pointer_capture: true,
keyboard_capture: true,
wheel_capture: true,
pointer_lock: true,
domain_hit_testing: true,
}
}
#[test]
fn canvas_host_capture_state_acquires_pointer_locked_canvas() {
let mut state = CanvasHostCaptureState::new();
let plan = capture_plan(
7,
"editor.mask.viewport",
UiRect::new(12.0, 16.0, 320.0, 180.0),
);
let transition = state.sync([plan.clone()]);
assert_eq!(state.active_plans(), std::slice::from_ref(&plan));
assert_eq!(transition.changes.len(), 1);
assert_eq!(
transition.changes[0].kind,
CanvasHostCaptureChangeKind::Acquired
);
assert_eq!(
transition.changes[0].id,
CanvasHostCaptureId::new(UiNodeId(7), "editor.mask.viewport")
);
assert_eq!(
transition.platform_requests(),
vec![
PlatformRequest::Cursor(CursorRequest::SetGrab(CursorGrabMode::Locked)),
PlatformRequest::Cursor(CursorRequest::SetVisible(false)),
]
);
let mut allocator = PlatformRequestIdAllocator::new(90);
let service_requests = transition.platform_service_requests(&mut allocator);
assert_eq!(
service_requests
.iter()
.map(|request| request.id)
.collect::<Vec<_>>(),
vec![PlatformRequestId::new(90), PlatformRequestId::new(91)]
);
assert_eq!(
service_requests
.iter()
.map(|request| request.request.clone())
.collect::<Vec<_>>(),
transition.platform_requests()
);
assert_eq!(allocator.next_value(), 92);
}
#[test]
fn canvas_host_capture_state_unchanged_plan_emits_no_transition() {
let mut state = CanvasHostCaptureState::new();
let plan = capture_plan(
7,
"editor.mask.viewport",
UiRect::new(12.0, 16.0, 320.0, 180.0),
);
assert!(!state.sync([plan.clone()]).is_empty());
let transition = state.sync([plan]);
assert!(transition.is_empty());
assert!(transition.platform_requests().is_empty());
}
#[test]
fn canvas_host_capture_state_updates_release_before_reacquire() {
let mut state = CanvasHostCaptureState::new();
let initial = capture_plan(
7,
"editor.mask.viewport",
UiRect::new(12.0, 16.0, 320.0, 180.0),
);
let moved = capture_plan(
7,
"editor.mask.viewport",
UiRect::new(24.0, 32.0, 400.0, 220.0),
);
state.sync([initial]);
let transition = state.sync([moved]);
assert_eq!(transition.changes.len(), 1);
assert_eq!(
transition.changes[0].kind,
CanvasHostCaptureChangeKind::Updated
);
assert_eq!(
transition.platform_requests(),
vec![
PlatformRequest::Cursor(CursorRequest::SetGrab(CursorGrabMode::None)),
PlatformRequest::Cursor(CursorRequest::SetVisible(true)),
PlatformRequest::Cursor(CursorRequest::SetGrab(CursorGrabMode::Locked)),
PlatformRequest::Cursor(CursorRequest::SetVisible(false)),
]
);
}
#[test]
fn canvas_host_capture_state_releases_missing_capture() {
let mut state = CanvasHostCaptureState::new();
state.sync([capture_plan(
7,
"editor.mask.viewport",
UiRect::new(12.0, 16.0, 320.0, 180.0),
)]);
let transition = state.sync([]);
assert!(state.is_empty());
assert_eq!(transition.changes.len(), 1);
assert_eq!(
transition.changes[0].kind,
CanvasHostCaptureChangeKind::Released
);
assert_eq!(
transition.platform_requests(),
vec![
PlatformRequest::Cursor(CursorRequest::SetGrab(CursorGrabMode::None)),
PlatformRequest::Cursor(CursorRequest::SetVisible(true)),
]
);
}
#[test]
fn canvas_host_capture_diagnostics_report_capture_lifecycle_and_capabilities() {
let mut state = CanvasHostCaptureState::new();
let plan = capture_plan(
7,
"editor.mask.viewport",
UiRect::new(12.0, 16.0, 320.0, 180.0),
);
let id = CanvasHostCaptureId::new(UiNodeId(7), "editor.mask.viewport");
let transition = state.sync([plan.clone()]);
let report = CanvasHostCaptureDiagnosticReport::from_state_transition(
&state,
&transition,
PlatformServiceCapabilities::DESKTOP,
);
assert!(report.has_kind(CanvasHostCaptureDiagnosticKind::Acquired));
let diagnostic = report.diagnostics_for(&id).next().unwrap();
assert_eq!(diagnostic.kind, CanvasHostCaptureDiagnosticKind::Acquired);
assert_eq!(diagnostic.node, Some(UiNodeId(7)));
assert_eq!(diagnostic.key.as_deref(), Some("editor.mask.viewport"));
assert_eq!(diagnostic.current.as_ref(), Some(&plan));
assert_eq!(
diagnostic.platform_requests,
vec![
PlatformRequest::Cursor(CursorRequest::SetGrab(CursorGrabMode::Locked)),
PlatformRequest::Cursor(CursorRequest::SetVisible(false)),
]
);
assert!(diagnostic.unsupported_requests.is_empty());
let unavailable = CanvasHostCaptureDiagnosticReport::from_state_transition(
&state,
&transition,
PlatformServiceCapabilities::NONE,
);
let unavailable = unavailable.diagnostics_for(&id).next().unwrap();
assert_eq!(
unavailable.kind,
CanvasHostCaptureDiagnosticKind::Unavailable
);
assert_eq!(
unavailable.unsupported_requests,
transition.platform_requests()
);
let active = CanvasHostCaptureDiagnosticReport::from_state_transition(
&state,
&CanvasHostCaptureTransition::new(),
PlatformServiceCapabilities::DESKTOP,
);
let active = active.diagnostics_for(&id).next().unwrap();
assert_eq!(active.kind, CanvasHostCaptureDiagnosticKind::Active);
assert_eq!(active.previous.as_ref(), Some(&plan));
assert_eq!(active.current.as_ref(), Some(&plan));
let release = state.sync([]);
let release_report = CanvasHostCaptureDiagnosticReport::from_state_transition(
&state,
&release,
PlatformServiceCapabilities::DESKTOP,
);
let release = release_report.diagnostics_for(&id).next().unwrap();
assert_eq!(release.kind, CanvasHostCaptureDiagnosticKind::Released);
assert_eq!(release.previous.as_ref(), Some(&plan));
assert_eq!(release.current, None);
}
#[test]
fn canvas_host_capture_diagnostics_attach_denied_cursor_responses_to_capture() {
let mut state = CanvasHostCaptureState::new();
let plan = capture_plan(
7,
"editor.mask.viewport",
UiRect::new(12.0, 16.0, 320.0, 180.0),
);
let id = CanvasHostCaptureId::new(UiNodeId(7), "editor.mask.viewport");
let transition = state.sync([plan.clone()]);
let mut allocator = PlatformRequestIdAllocator::new(400);
let service_requests = transition.platform_service_requests(&mut allocator);
let responses = vec![service_requests[0].unsupported_response()];
let report = CanvasHostCaptureDiagnosticReport::from_state_transition(
&state,
&transition,
PlatformServiceCapabilities::DESKTOP,
)
.with_platform_responses(&service_requests, &responses);
assert!(report.has_kind(CanvasHostCaptureDiagnosticKind::Denied));
let denied = report
.diagnostics
.iter()
.find(|diagnostic| diagnostic.kind == CanvasHostCaptureDiagnosticKind::Denied)
.unwrap();
assert_eq!(denied.id, Some(id));
assert_eq!(denied.node, Some(UiNodeId(7)));
assert_eq!(denied.key.as_deref(), Some("editor.mask.viewport"));
assert_eq!(denied.current.as_ref(), Some(&plan));
assert_eq!(
denied.platform_requests,
vec![service_requests[0].request.clone()]
);
assert_eq!(denied.platform_responses, responses);
}
#[derive(Debug, Default)]
struct CanvasBackend {
rendered: Vec<String>,
scale_factors: Vec<f32>,
focused: Vec<bool>,
dirty: Vec<bool>,
}
#[derive(Debug)]
struct RecordingCanvasHandler;
impl CanvasRenderHandler<CanvasBackend> for RecordingCanvasHandler {
fn render_canvas(
&mut self,
context: CanvasRenderContext<'_, CanvasBackend>,
) -> Result<CanvasRenderOutput, RenderError> {
context
.backend
.rendered
.push(context.request.canvas.key.clone());
context.backend.scale_factors.push(context.scale_factor);
context.backend.focused.push(context.interaction.focused);
context.backend.dirty.push(context.is_dirty());
let mut output = CanvasRenderOutput::new()
.dirty_region(context.request.rect)
.repaint_requested(context.interaction.focused);
if context.request.canvas.interaction.domain_hit_testing {
output = output.hit_targets([
CanvasHitTarget::new("background", context.request.rect)
.label("Background")
.z_index(1),
CanvasHitTarget::new("disabled-overlay", context.request.rect)
.label("Disabled overlay")
.disabled(true)
.z_index(10),
CanvasHitTarget::new("primary-range", context.request.rect)
.label("Primary range")
.value("active")
.metadata("kind", "range")
.z_index(4),
]);
}
Ok(output)
}
}
#[test]
fn canvas_render_registry_dispatches_requests_with_context() {
let canvas = CanvasContent::new("editor.mask.viewport")
.callback()
.pointer_capture(true)
.keyboard_capture(true)
.domain_hit_testing(true);
let mut paint = PaintList::default();
paint.items.push(paint_item(
7,
UiRect::new(12.0, 16.0, 320.0, 180.0),
PaintKind::Canvas(canvas),
));
let request = RenderFrameRequest::new(
RenderTarget::window("main", UiSize::new(640.0, 480.0)),
UiSize::new(640.0, 480.0),
paint,
)
.options(RenderOptions {
scale_factor: 2.0,
..RenderOptions::default()
})
.node_interaction(
UiNodeId(7),
HostNodeInteraction {
focused: true,
..HostNodeInteraction::default()
},
);
let mut backend = CanvasBackend::default();
let mut registry = CanvasRenderRegistry::new();
assert!(!registry.register("editor.mask.viewport", RecordingCanvasHandler));
let report = registry
.render_frame_canvases_strict(&request, &mut backend)
.expect("canvas dispatch");
assert_eq!(report.rendered_count(), 1);
assert_eq!(report.missing_count(), 0);
assert_eq!(report.failed_count(), 0);
assert!(report.repaint_requested());
assert_eq!(backend.rendered, vec!["editor.mask.viewport".to_string()]);
assert_eq!(backend.scale_factors, vec![2.0]);
assert_eq!(backend.focused, vec![true]);
assert_eq!(backend.dirty, vec![true]);
assert_eq!(report.outcomes[0].request().node, UiNodeId(7));
assert_eq!(report.resource_updates(), Vec::<ResourceUpdate>::new());
let hit_collections = report.hit_collections();
assert_eq!(hit_collections.len(), 1);
assert_eq!(hit_collections[0].node, UiNodeId(7));
assert_eq!(hit_collections[0].key, "editor.mask.viewport");
assert_eq!(hit_collections[0].len(), 3);
assert_eq!(hit_collections[0].targets[2].id, "primary-range");
assert_eq!(
hit_collections[0].targets[2].metadata,
vec![("kind".to_string(), "range".to_string())]
);
assert_eq!(
hit_collections[0].topmost_at(UiPoint::new(20.0, 24.0)),
Some(&hit_collections[0].targets[2])
);
assert_eq!(report.hit_targets().len(), 3);
}
#[test]
fn canvas_render_registry_reports_missing_handlers() {
let mut paint = PaintList::default();
paint.items.push(paint_item(
4,
UiRect::new(0.0, 0.0, 120.0, 80.0),
PaintKind::Canvas(CanvasContent::new("missing.viewport").native_viewport()),
));
let request = RenderFrameRequest::new(
RenderTarget::app_owned("main", UiSize::new(640.0, 480.0)),
UiSize::new(640.0, 480.0),
paint,
);
let mut backend = CanvasBackend::default();
let mut registry = CanvasRenderRegistry::new();
let report = registry.render_frame_canvases(&request, &mut backend);
assert_eq!(report.rendered_count(), 0);
assert_eq!(report.missing_count(), 1);
assert_eq!(
report.first_missing().unwrap().canvas.key,
"missing.viewport"
);
assert_eq!(
report.into_strict_result().unwrap_err(),
RenderError::MissingCanvasRenderer("missing.viewport".to_string())
);
}
#[test]
fn canvas_hit_targets_export_accessibility_metadata() {
let target = CanvasHitTarget::new("item.7", UiRect::new(10.0, 12.0, 30.0, 16.0))
.label("Selected item")
.value("ready")
.metadata("Layer", "foreground")
.z_index(2);
let disabled =
CanvasHitTarget::new("disabled", UiRect::new(0.0, 0.0, 80.0, 80.0)).disabled(true);
let collection = CanvasHitCollection::new(UiNodeId(5), "editor.viewport")
.target(disabled.clone())
.target(target.clone());
let meta = target.accessibility_meta(1, 2, true);
assert_eq!(meta.role, AccessibilityRole::ListItem);
assert_eq!(meta.label.as_deref(), Some("Selected item"));
assert_eq!(meta.value.as_deref(), Some("ready; target 2 of 2"));
assert_eq!(meta.selected, Some(true));
assert!(meta.focusable);
let summary_text = meta.summary.unwrap().screen_reader_text();
assert!(summary_text.contains("Target id: item.7"));
assert!(summary_text.contains("Layer: foreground"));
let disabled_meta = disabled.accessibility_meta(0, 2, false);
assert!(!disabled_meta.enabled);
assert!(!disabled_meta.focusable);
assert_eq!(
collection.topmost_at(UiPoint::new(14.0, 14.0)),
Some(&target)
);
let collection_meta = collection.accessibility_meta("Canvas hits");
assert_eq!(collection_meta.role, AccessibilityRole::List);
assert_eq!(
collection_meta.value.as_deref(),
Some("2 targets; 1 enabled")
);
let collection_text = collection_meta.summary.unwrap().screen_reader_text();
assert!(collection_text.contains("Canvas key: editor.viewport"));
assert!(collection_text.contains("Labelled targets: 1"));
}
#[test]
fn canvas_hit_topmost_prefers_later_targets_for_equal_z() {
let first = CanvasHitTarget::new("first", UiRect::new(0.0, 0.0, 40.0, 40.0)).z_index(2);
let second = CanvasHitTarget::new("second", UiRect::new(0.0, 0.0, 40.0, 40.0)).z_index(2);
let lower = CanvasHitTarget::new("lower", UiRect::new(0.0, 0.0, 40.0, 40.0)).z_index(1);
let disabled_top = CanvasHitTarget::new("disabled-top", UiRect::new(0.0, 0.0, 40.0, 40.0))
.z_index(100)
.disabled(true);
let collection = CanvasHitCollection::new(UiNodeId(2), "editor.viewport")
.target(first)
.target(lower)
.target(second.clone())
.target(disabled_top);
assert_eq!(
collection.topmost_at(UiPoint::new(12.0, 12.0)),
Some(&second)
);
}
#[test]
fn render_request_extracts_image_requests_for_nodes_and_scene_placements() {
let mut paint = PaintList::default();
paint.items.push(paint_item(
2,
UiRect::new(8.0, 10.0, 24.0, 18.0),
PaintKind::Image {
key: "icons.play".to_string(),
tint: Some(ColorRgba::new(120, 180, 255, 255)),
},
));
paint.items.push(paint_item(
3,
UiRect::new(40.0, 12.0, 64.0, 48.0),
PaintKind::ImagePlacement(
PaintImage::new("covers.scale", UiRect::new(40.0, 12.0, 64.0, 48.0))
.fit(ImageFit::Contain)
.align(ImageAlignment::End, ImageAlignment::Start),
),
));
let request = RenderFrameRequest::new(
RenderTarget::window("main", UiSize::new(160.0, 90.0)),
UiSize::new(160.0, 90.0),
paint,
);
let images = request.image_requests();
assert_eq!(images.len(), 2);
assert_eq!(images[0].kind, ImageRenderKind::NodeImage);
assert_eq!(images[0].key(), "icons.play");
assert_eq!(images[0].image.rect, UiRect::new(8.0, 10.0, 24.0, 18.0));
assert_eq!(
images[0].image.tint,
Some(ColorRgba::new(120, 180, 255, 255))
);
assert_eq!(images[1].kind, ImageRenderKind::ImagePlacement);
assert_eq!(images[1].key(), "covers.scale");
assert_eq!(images[1].image.fit, ImageFit::Contain);
assert_eq!(images[1].image.horizontal_align, ImageAlignment::End);
}
#[derive(Debug, Default)]
struct ImageBackend {
rendered: Vec<String>,
scale_factors: Vec<f32>,
hovered: Vec<bool>,
dirty: Vec<bool>,
}
#[derive(Debug)]
struct RecordingImageHandler;
impl ImageRenderHandler<ImageBackend> for RecordingImageHandler {
fn render_image(
&mut self,
context: ImageRenderContext<'_, ImageBackend>,
) -> Result<ImageRenderOutput, RenderError> {
context
.backend
.rendered
.push(context.request.image.key.clone());
context.backend.scale_factors.push(context.scale_factor);
context.backend.hovered.push(context.interaction.hovered);
context.backend.dirty.push(context.is_dirty());
Ok(ImageRenderOutput::new()
.dirty_region(context.request.image.rect)
.repaint_requested(context.interaction.hovered))
}
}
#[test]
fn image_render_registry_dispatches_requests_with_context() {
let mut paint = PaintList::default();
paint.items.push(paint_item(
5,
UiRect::new(10.0, 12.0, 32.0, 32.0),
PaintKind::Image {
key: "icons.record".to_string(),
tint: None,
},
));
let request = RenderFrameRequest::new(
RenderTarget::window("main", UiSize::new(160.0, 90.0)),
UiSize::new(160.0, 90.0),
paint,
)
.options(RenderOptions {
scale_factor: 1.5,
..RenderOptions::default()
})
.node_interaction(
UiNodeId(5),
HostNodeInteraction {
hovered: true,
..HostNodeInteraction::default()
},
);
let mut backend = ImageBackend::default();
let mut registry = ImageRenderRegistry::new();
assert!(!registry.register("icons.record", RecordingImageHandler));
let report = registry
.render_frame_images_strict(&request, &mut backend)
.expect("image dispatch");
assert_eq!(report.rendered_count(), 1);
assert_eq!(report.missing_count(), 0);
assert_eq!(report.failed_count(), 0);
assert!(report.repaint_requested());
assert_eq!(backend.rendered, vec!["icons.record".to_string()]);
assert_eq!(backend.scale_factors, vec![1.5]);
assert_eq!(backend.hovered, vec![true]);
assert_eq!(backend.dirty, vec![true]);
assert_eq!(report.outcomes[0].request().node, UiNodeId(5));
assert_eq!(report.resource_updates(), Vec::<ResourceUpdate>::new());
}
#[test]
fn image_render_registry_reports_missing_handlers() {
let mut paint = PaintList::default();
paint.items.push(paint_item(
8,
UiRect::new(0.0, 0.0, 120.0, 80.0),
PaintKind::Image {
key: "missing.image".to_string(),
tint: None,
},
));
let request = RenderFrameRequest::new(
RenderTarget::app_owned("main", UiSize::new(640.0, 480.0)),
UiSize::new(640.0, 480.0),
paint,
);
let mut backend = ImageBackend::default();
let mut registry = ImageRenderRegistry::new();
let report = registry.render_frame_images(&request, &mut backend);
assert_eq!(report.rendered_count(), 0);
assert_eq!(report.missing_count(), 1);
assert_eq!(report.first_missing().unwrap().image.key, "missing.image");
assert_eq!(
report.into_strict_result().unwrap_err(),
RenderError::MissingImageRenderer("missing.image".to_string())
);
}
#[derive(Debug, Default)]
struct TestResolver {
descriptor: Option<ResourceDescriptor>,
}
impl ResourceResolver for TestResolver {
fn resolve_resource(&self, id: &ResourceId) -> Option<ResourceDescriptor> {
self.descriptor
.clone()
.filter(|descriptor| descriptor.handle.id() == id)
}
}
#[derive(Debug)]
struct RecordingRenderer {
capabilities: BackendCapabilities,
resolved: Vec<ResourceId>,
}
impl RendererAdapter for RecordingRenderer {
fn capabilities(&self) -> BackendCapabilities {
self.capabilities.clone()
}
fn render_frame(
&mut self,
request: RenderFrameRequest,
resolver: &dyn ResourceResolver,
) -> Result<RenderFrameOutput, RenderError> {
if matches!(request.target.kind(), RenderTargetKind::Snapshot)
&& !self.capabilities.rendering.deterministic_snapshots
{
return Err(RenderError::UnsupportedTarget(request.target.kind()));
}
for update in &request.resource_updates {
if !self
.capabilities
.supports_resource(update.descriptor.handle.kind())
{
return Err(RenderError::UnsupportedResource(
update.descriptor.handle.kind(),
));
}
if !update.has_expected_byte_len() || !update.dirty_rect_is_valid() {
return Err(RenderError::InvalidResourceUpdate(
update.descriptor.handle.id().key.clone(),
));
}
let id = update.descriptor.handle.id().clone();
resolver
.resolve_resource(&id)
.ok_or_else(|| RenderError::MissingResource(id.clone()))?;
self.resolved.push(id);
}
let batches = request.batches();
let mut output = RenderFrameOutput::new(request.target);
output.painted_items = request.paint.items.len();
output.batches = batches;
output.dirty_regions = request.dirty_regions;
output.timings = FrameTiming::new().section("paint-build", Duration::from_millis(1));
Ok(output)
}
}
#[test]
fn renderer_adapter_trait_receives_resources_batches_and_timings() {
let handle = ResourceHandle::Image(ImageHandle::app("cover"));
let descriptor =
ResourceDescriptor::new(handle.clone(), PixelSize::new(2, 2), ResourceFormat::Rgba8);
let update = ResourceUpdate::full(descriptor.clone(), vec![128; 2 * 2 * 4]);
let resolver = TestResolver {
descriptor: Some(descriptor),
};
let paint = PaintList {
items: vec![paint_item(
0,
UiRect::new(0.0, 0.0, 16.0, 16.0),
PaintKind::Image {
key: "cover".to_string(),
tint: None,
},
)],
};
let request = RenderFrameRequest::new(
RenderTarget::snapshot(PixelSize::new(64, 64)),
UiSize::new(64.0, 64.0),
paint,
)
.resource_update(update)
.options(RenderOptions {
deterministic: true,
..RenderOptions::default()
});
let mut renderer = RecordingRenderer {
capabilities: BackendCapabilities::new("recording")
.adapter(BackendAdapterKind::Test)
.resources(ResourceCapabilities {
images: true,
partial_texture_updates: true,
..ResourceCapabilities::NONE
})
.rendering(RenderingCapabilities {
deterministic_snapshots: true,
offscreen: true,
partial_updates: true,
high_dpi: true,
..RenderingCapabilities::NONE
}),
resolved: Vec::new(),
};
let output = renderer
.render_frame(request, &resolver)
.expect("render output");
assert_eq!(output.painted_items, 1);
assert_eq!(output.batches.len(), 1);
assert_eq!(
renderer.resolved,
vec![ResourceId::new(ResourceDomain::App, "cover")]
);
assert_eq!(
output.timings.duration("paint-build"),
Some(Duration::from_millis(1))
);
}
}