use alloc::sync::Arc;
use crate::core::{Color, HorizontalAlignment, Point, Rect};
use crate::event::{Event, EventHandler};
use crate::impl_widget_property_hooks;
use crate::property_names_of;
use crate::render::RenderContext;
use crate::signal::{GenericSignal, Signal1};
use crate::widget::capability::coercion::{expect_bool, expect_string};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::metrics::role_font;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
const EMPTY_VIEWFINDER: Color = Color::rgba(50, 50, 60, 255);
const ACTIVE_VIEWFINDER: Color = Color::rgba(30, 30, 40, 255);
struct ProcessedFrame {
ptr: usize,
len: usize,
width: u32,
height: u32,
stride: u32,
mirror: bool,
zoom_bits: u32,
data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CameraDevice {
pub id: u32,
pub label: String,
pub resolutions: Vec<(u32, u32)>,
}
impl CameraDevice {
pub fn new(id: u32, label: impl Into<String>) -> Self {
Self { id, label: label.into(), resolutions: Vec::new() }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PixelFormat {
#[default]
Rgba8888,
Bgra8888,
Nv12,
Yuy2,
}
#[derive(Debug, Clone)]
pub struct CameraFrame {
pub width: u32,
pub height: u32,
pub pts_ms: u64,
pub format: PixelFormat,
pub stride: u32,
pub data: Arc<[u8]>,
}
impl CameraFrame {
pub fn new(width: u32, height: u32, pts_ms: u64, data: Arc<[u8]>) -> Self {
Self::from_shared(
width,
height,
pts_ms,
PixelFormat::Rgba8888,
width.saturating_mul(4),
data,
)
}
pub fn from_shared(
width: u32,
height: u32,
pts_ms: u64,
format: PixelFormat,
stride: u32,
data: Arc<[u8]>,
) -> Self {
Self { width, height, pts_ms, format, stride, data }
}
pub fn expected_len(&self) -> usize {
self.stride as usize * self.height as usize
}
}
pub struct CameraPreview {
base: BaseWidget,
is_active: bool,
resolution: (u32, u32),
camera_id: u32,
mirror_mode: bool,
show_controls: bool,
zoom_level: f32,
back: Option<Arc<CameraFrame>>,
front: Option<Arc<CameraFrame>>,
pub error_occurred: Signal1<String>,
pub permission_denied: GenericSignal,
pub frame_available: Signal1<u64>,
pub frame_presented: Signal1<f64>,
available_cameras: Vec<CameraDevice>,
pixel_format: PixelFormat,
target_fps: u32,
pub format_negotiated: Signal1<(u32, u32)>,
resolution_text: String,
camera_id_text: String,
zoom_text: String,
processed: Option<ProcessedFrame>,
}
impl CameraPreview {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::CameraPreview, geometry, "CameraPreview"),
is_active: false,
resolution: (640, 480),
camera_id: 0,
mirror_mode: false,
show_controls: true,
zoom_level: 1.0,
back: None,
front: None,
error_occurred: Signal1::new(),
permission_denied: GenericSignal::new(),
frame_available: Signal1::new(),
frame_presented: Signal1::new(),
available_cameras: Vec::new(),
pixel_format: PixelFormat::Rgba8888,
target_fps: 30,
format_negotiated: Signal1::new(),
resolution_text: "640x480".to_string(),
camera_id_text: "Camera #0".to_string(),
zoom_text: "1.0x".to_string(),
processed: None,
}
}
pub fn start_preview(&mut self) {
self.is_active = true;
self.base.request_redraw();
}
pub fn stop_preview(&mut self) {
self.is_active = false;
self.base.request_redraw();
}
pub fn is_active(&self) -> bool {
self.is_active
}
pub fn toggle_preview(&mut self) {
if self.is_active {
self.stop_preview();
} else {
self.start_preview();
}
}
pub fn set_camera_id(&mut self, id: u32) -> Result<(), String> {
if !self.available_cameras.is_empty()
&& !self.available_cameras.iter().any(|device| device.id == id)
{
let message = format!("camera {id} is not among the enumerated devices");
self.error_occurred.emit(message.clone());
return Err(message);
}
if self.camera_id == id {
return Ok(());
}
self.camera_id = id;
self.camera_id_text = format!("Camera #{id}");
self.base.request_redraw();
Ok(())
}
pub fn set_available_cameras(&mut self, devices: Vec<CameraDevice>) {
self.available_cameras = devices;
self.base.request_redraw();
}
pub fn available_cameras(&self) -> &[CameraDevice] {
&self.available_cameras
}
pub fn pixel_format(&self) -> PixelFormat {
self.pixel_format
}
pub fn set_pixel_format(&mut self, format: PixelFormat) {
if self.pixel_format == format {
return;
}
self.pixel_format = format;
self.announce_format();
self.base.request_redraw();
}
pub fn target_fps(&self) -> u32 {
self.target_fps
}
pub fn set_target_fps(&mut self, fps: u32) {
let fps = fps.max(1);
if self.target_fps == fps {
return;
}
self.target_fps = fps;
self.announce_format();
self.base.request_redraw();
}
fn announce_format(&mut self) {
self.format_negotiated.emit(self.resolution);
}
pub fn camera_id(&self) -> u32 {
self.camera_id
}
pub fn set_resolution(&mut self, w: u32, h: u32) {
let resolution = (w.max(1), h.max(1));
if self.resolution == resolution {
return;
}
self.resolution = resolution;
self.resolution_text = format!("{}x{}", resolution.0, resolution.1);
self.processed = None;
self.announce_format();
self.base.request_redraw();
}
pub fn resolution(&self) -> (u32, u32) {
self.resolution
}
pub fn set_mirror_mode(&mut self, mirrored: bool) {
if self.mirror_mode == mirrored {
return;
}
self.mirror_mode = mirrored;
self.processed = None;
self.base.request_redraw();
}
pub fn is_mirror_mode(&self) -> bool {
self.mirror_mode
}
pub fn set_zoom(&mut self, level: f32) {
let level = level.clamp(1.0, 10.0);
if self.zoom_level == level {
return;
}
self.zoom_level = level;
self.zoom_text = format!("{level:.1}x");
self.processed = None;
self.base.request_redraw();
}
pub fn set_frame(&mut self, width: u32, height: u32, data: &[u8]) -> Result<(), String> {
let result = self.set_frame_bytes_shared(width, height, 0, Arc::from(data));
if let Err(error) = &result {
self.error_occurred.emit(error.clone());
}
result
}
pub fn set_frame_bytes_shared(
&mut self,
width: u32,
height: u32,
pts_ms: u64,
data: Arc<[u8]>,
) -> Result<(), String> {
self.set_frame_shared_with_format(
width,
height,
pts_ms,
self.pixel_format,
width.saturating_mul(4),
data,
)
}
pub fn set_frame_shared(&mut self, frame: Arc<CameraFrame>) -> Result<(), String> {
self.validate_frame(&frame)?;
self.publish_frame(frame);
Ok(())
}
fn validate_frame(&self, frame: &CameraFrame) -> Result<(), String> {
if frame.width == 0 || frame.height == 0 {
return Err("a camera frame needs non-zero dimensions".to_string());
}
if self.pixel_format != PixelFormat::Rgba8888 || frame.format != PixelFormat::Rgba8888 {
return Err(format!(
"the negotiated {:?} layout cannot be painted; this control accepts RGBA8888 frames",
frame.format
));
}
let minimum = frame.width as usize * 4;
if (frame.stride as usize) < minimum {
return Err(format!(
"a {}x{} RGBA frame needs a stride of at least {minimum} bytes, got {}",
frame.width, frame.height, frame.stride
));
}
let expected = frame.expected_len();
if frame.data.len() != expected {
return Err(format!(
"a {}x{} RGBA frame at stride {} needs {expected} bytes, got {}",
frame.width,
frame.height,
frame.stride,
frame.data.len()
));
}
Ok(())
}
fn publish_frame(&mut self, frame: Arc<CameraFrame>) {
let resolution = (frame.width, frame.height);
let negotiated = self.resolution != resolution;
self.resolution = resolution;
self.resolution_text = format!("{}x{}", resolution.0, resolution.1);
self.processed = None;
self.frame_available.emit(frame.pts_ms);
self.back = Some(frame);
if negotiated {
self.announce_format();
}
self.base.request_redraw();
}
pub fn set_frame_shared_with_format(
&mut self,
width: u32,
height: u32,
pts_ms: u64,
format: PixelFormat,
stride: u32,
data: Arc<[u8]>,
) -> Result<(), String> {
let frame = Arc::new(CameraFrame::from_shared(width, height, pts_ms, format, stride, data));
self.validate_frame(&frame)?;
self.publish_frame(frame);
Ok(())
}
pub fn present_frame(&mut self) -> bool {
let Some(frame) = self.back.take() else {
return false;
};
let pts = frame.pts_ms;
self.front = Some(frame);
self.frame_presented.emit(pts as f64 / 1000.0);
self.base.request_redraw();
true
}
pub fn clear_frame(&mut self) {
self.front = None;
self.back = None;
self.processed = None;
self.base.request_redraw();
}
pub fn frame(&self) -> Option<&CameraFrame> {
self.front.as_deref()
}
pub fn pending_frame(&self) -> Option<&CameraFrame> {
self.back.as_deref()
}
pub fn report_error(&mut self, message: impl Into<String>) {
self.error_occurred.emit(message.into());
}
pub fn report_permission_denied(&mut self) {
self.permission_denied.emit();
}
pub fn zoom_level(&self) -> f32 {
self.zoom_level
}
pub fn zoom_in(&mut self) {
self.set_zoom(self.zoom_level + 0.5);
}
pub fn zoom_out(&mut self) {
self.set_zoom(self.zoom_level - 0.5);
}
pub fn show_controls(&mut self) {
self.show_controls = true;
self.base.request_redraw();
}
pub fn hide_controls(&mut self) {
self.show_controls = false;
self.base.request_redraw();
}
pub fn controls_visible(&self) -> bool {
self.show_controls
}
fn zoom_in_rect(&self, rect: Rect) -> Rect {
Rect::new(rect.x + rect.width as i32 - 42, rect.y + 6, 36, 36)
}
fn zoom_out_rect(&self, rect: Rect) -> Rect {
Rect::new(rect.x + rect.width as i32 - 42, rect.y + 46, 36, 36)
}
fn ensure_processed(&mut self, frame: &CameraFrame) -> bool {
let tight = frame.width as usize * 4;
let identity = !self.mirror_mode
&& (self.zoom_level - 1.0).abs() < f32::EPSILON
&& frame.stride as usize == tight;
if identity {
return false;
}
let ptr = frame.data.as_ptr() as usize;
let len = frame.data.len();
let zoom_bits = self.zoom_level.to_bits();
let reusable = matches!(
self.processed.as_ref(),
Some(cached)
if cached.ptr == ptr
&& cached.len == len
&& cached.width == frame.width
&& cached.height == frame.height
&& cached.stride == frame.stride
&& cached.mirror == self.mirror_mode
&& cached.zoom_bits == zoom_bits
);
if reusable {
return true;
}
let width = frame.width;
let height = frame.height;
let zoom = self.zoom_level.max(1.0) as f64;
let crop_width = ((width as f64 / zoom).round() as u32).clamp(1, width.max(1));
let crop_height = ((height as f64 / zoom).round() as u32).clamp(1, height.max(1));
let crop_x = (width.saturating_sub(crop_width)) / 2;
let crop_y = (height.saturating_sub(crop_height)) / 2;
let mut data = crate::compat::vec![0u8; tight * height as usize];
for row in 0..height {
let source_y = crop_y + (row as u64 * crop_height as u64 / height.max(1) as u64) as u32;
let source_row = source_y as usize * frame.stride as usize;
for column in 0..width {
let inner = (column as u64 * crop_width as u64 / width.max(1) as u64) as u32;
let source_x = if self.mirror_mode {
crop_x + crop_width.saturating_sub(1).saturating_sub(inner)
} else {
crop_x + inner
};
let from = source_row + source_x as usize * 4;
let to = row as usize * tight + column as usize * 4;
if let Some(pixel) = frame.data.get(from..from + 4) {
data[to..to + 4].copy_from_slice(pixel);
}
}
}
self.processed = Some(ProcessedFrame {
ptr,
len,
width,
height,
stride: frame.stride,
mirror: self.mirror_mode,
zoom_bits,
data,
});
true
}
}
impl Widget for CameraPreview {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(320, 240)
}
fn as_draw_mut(&mut self) -> Option<&mut dyn crate::widget::Draw> {
Some(self)
}
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"error_occurred" => {
Some(EventSignalRef::mapped("error_occurred", &self.error_occurred, |v| {
CapabilityValue::String(v.clone())
}))
}
"permission_denied" => {
Some(EventSignalRef::unit("permission_denied", &self.permission_denied))
}
"frame_available" => {
Some(EventSignalRef::mapped("frame_available", &self.frame_available, |v| {
CapabilityValue::UInt(*v)
}))
}
"frame_presented" => {
Some(EventSignalRef::mapped("frame_presented", &self.frame_presented, |v| {
CapabilityValue::Float(*v)
}))
}
"format_negotiated" => Some(EventSignalRef::mapped(
"format_negotiated",
&self.format_negotiated,
|(width, height)| {
CapabilityValue::Tuple(crate::compat::Vec::from([
CapabilityValue::UInt(*width as u64),
CapabilityValue::UInt(*height as u64),
]))
},
)),
_ => None,
}
}
impl_widget_property_hooks!();
}
impl WidgetProperties for CameraPreview {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"is_active" => Ok(CapabilityValue::Bool(self.is_active())),
"pixel_format" => Ok(CapabilityValue::String(format!("{:?}", self.pixel_format()))),
"target_fps" => Ok(CapabilityValue::UInt(self.target_fps() as u64)),
"camera_id" => Ok(CapabilityValue::UInt(self.camera_id() as u64)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"is_active" => {
if expect_bool(value)? {
self.start_preview();
} else {
self.stop_preview();
}
Ok(())
}
"pixel_format" => {
let format = match expect_string(value)?.to_ascii_lowercase().as_str() {
"rgba8888" | "rgba" => PixelFormat::Rgba8888,
"bgra8888" | "bgra" => PixelFormat::Bgra8888,
"nv12" => PixelFormat::Nv12,
"yuy2" => PixelFormat::Yuy2,
_ => return Err(CapabilityAccessError::TypeMismatch),
};
self.set_pixel_format(format);
Ok(())
}
"target_fps" => {
let fps = match value {
CapabilityValue::UInt(fps) => fps.min(u32::MAX as u64) as u32,
_ => return Err(CapabilityAccessError::TypeMismatch),
};
self.set_target_fps(fps);
Ok(())
}
"camera_id" => {
let id = match value {
CapabilityValue::UInt(id) => id.min(u32::MAX as u64) as u32,
_ => return Err(CapabilityAccessError::TypeMismatch),
};
self.set_camera_id(id).map_err(|_| CapabilityAccessError::OutOfRange)
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"is_active",
"pixel_format",
"target_fps",
"camera_id",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for CameraPreview {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let w = rect.width as i32;
let h = rect.height as i32;
if w <= 0 || h <= 0 {
return;
}
let style = self.base.style();
let small_font = role_font(style, 11.0);
let normal_font = role_font(style, 13.0);
let theme = crate::style::resolved_theme_style_shared("camera_preview");
let viewfinder = style
.background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
.unwrap_or(ACTIVE_VIEWFINDER);
let stage_ink = viewfinder.contrast_color();
let label_ink = viewfinder.blend(&stage_ink, 0.78);
let strong_ink = viewfinder.blend(&stage_ink, 0.86);
let crosshair_ink = viewfinder.blend(&stage_ink, 0.55);
if self.is_active {
context.fill_rect(rect, viewfinder);
if let Some(frame) = self.front.clone() {
let dx = rect.x + (w - frame.width as i32) / 2;
let dy = rect.y + (h - frame.height as i32) / 2;
context.push_clip(rect.x, rect.y, rect.width, rect.height);
let needs_processing = self.ensure_processed(&frame);
let pixels: &[u8] = match self.processed.as_ref() {
Some(processed) if needs_processing => &processed.data,
_ => frame.data.as_ref(),
};
context.draw_image(dx.max(0), dy.max(0), frame.width, frame.height, pixels);
context.pop_clip();
}
context.draw_text(
Point::new(rect.x + 6, rect.y + 14),
&self.resolution_text,
&small_font,
label_ink,
HorizontalAlignment::Left,
);
context.draw_text(
Point::new(rect.x + 6, rect.y + h - 10),
&self.camera_id_text,
&small_font,
label_ink,
HorizontalAlignment::Left,
);
context.draw_text(
Point::new(rect.x + w - 40, rect.y + 14),
&self.zoom_text,
&normal_font,
strong_ink,
HorizontalAlignment::Left,
);
if self.mirror_mode {
context.draw_text(
Point::new(rect.x + w / 2 - 20, rect.y + h - 10),
"MIRROR",
&small_font,
viewfinder.blend(&stage_ink, 0.68),
HorizontalAlignment::Left,
);
}
let cx = rect.x + w / 2;
let cy = rect.y + h / 2;
let crosshair_color = crosshair_ink;
context.draw_line(Point::new(cx, cy - 15), Point::new(cx, cy + 15), crosshair_color);
context.draw_line(Point::new(cx - 15, cy), Point::new(cx + 15, cy), crosshair_color);
let border_color = Color::rgba(0, 200, 50, 200);
context.draw_rect_stroke(rect, border_color, 2);
if self.show_controls {
let control_bg = Rect::new(rect.x + w - 44, rect.y + 4, 40, 80);
context.fill_rect(control_bg, Color::rgba(0, 0, 0, 120));
let zoom_in_btn = self.zoom_in_rect(rect);
context.fill_rect(zoom_in_btn, Color::rgba(255, 255, 255, 80));
let zoom_out_btn = self.zoom_out_rect(rect);
context.fill_rect(zoom_out_btn, Color::rgba(255, 255, 255, 80));
let glyph_ink = strong_ink;
let plus_center = zoom_in_btn.center();
context.draw_line(
Point::new(plus_center.x - 7, plus_center.y),
Point::new(plus_center.x + 7, plus_center.y),
glyph_ink,
);
context.draw_line(
Point::new(plus_center.x, plus_center.y - 7),
Point::new(plus_center.x, plus_center.y + 7),
glyph_ink,
);
let minus_center = zoom_out_btn.center();
context.draw_line(
Point::new(minus_center.x - 7, minus_center.y),
Point::new(minus_center.x + 7, minus_center.y),
glyph_ink,
);
}
let dot_size = 8;
let dot_rect = Rect::new(rect.x + 6, rect.y + 6, dot_size, dot_size);
context.fill_rect(dot_rect, Color::rgba(0, 255, 0, 255));
} else {
context.fill_rect(rect, EMPTY_VIEWFINDER);
let icon_w = 48u32;
let icon_h = 36u32;
let icon_x = rect.x + (w - icon_w as i32) / 2;
let icon_y = rect.y + (h - icon_h as i32) / 2 - 10;
let icon_rect = Rect::new(icon_x, icon_y, icon_w, icon_h);
context.fill_rounded_rect(icon_rect, 6, Color::rgba(100, 100, 120, 200));
let lens_center_x = icon_x + icon_w as i32 / 2;
let lens_center_y = icon_y + icon_h as i32 / 2;
let lens_rect = Rect::new(lens_center_x - 8, lens_center_y - 8, 16, 16);
context.fill_rounded_rect(lens_rect, 8, Color::rgba(70, 70, 90, 255));
let flash_rect = Rect::new(icon_x + icon_w as i32 - 10, icon_y + 4, 6, 6);
context.fill_rounded_rect(flash_rect, 3, Color::rgba(200, 200, 200, 150));
let empty_surface = EMPTY_VIEWFINDER;
context.draw_text(
Point::new(rect.x + w / 2 - 30, rect.y + h / 2 + 20),
"Camera Off",
&normal_font,
empty_surface.contrast_color().with_alpha(200),
HorizontalAlignment::Left,
);
context.draw_rect_stroke(rect, Color::rgba(100, 100, 110, 200), 1);
}
}
}
impl EventHandler for CameraPreview {
fn handle_event(&mut self, event: &Event) {
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos, button, .. } => {
if *button != 1 || !self.geometry().contains_point(*pos) {
return;
}
let rect = self.geometry();
if self.show_controls {
if self.zoom_in_rect(rect).contains_point(*pos) {
self.zoom_in();
return;
}
if self.zoom_out_rect(rect).contains_point(*pos) {
self.zoom_out();
return;
}
}
self.toggle_preview();
}
_ => {
self.base.handle_event(event);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn published_camera_events_resolve_to_signals() {
let preview = CameraPreview::new(Rect::new(0, 0, 320, 240));
for name in [
"error_occurred",
"permission_denied",
"frame_available",
"frame_presented",
"format_negotiated",
] {
assert!(preview.event_signal_dyn(name).is_some(), "{name} must resolve");
}
assert!(preview.event_signal_dyn("no_such_event").is_none());
}
#[test]
fn a_shared_frame_is_adopted_without_copying_and_announced() {
let mut preview = CameraPreview::new(Rect::new(0, 0, 320, 240));
preview.start_preview();
let seen = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(u64::MAX));
let s = seen.clone();
preview.frame_available.connect(move |pts: std::sync::Arc<u64>| {
s.store(*pts, std::sync::atomic::Ordering::SeqCst);
});
let buffer: std::sync::Arc<[u8]> = std::sync::Arc::from(vec![7u8; 4 * 4 * 4]);
let frame = std::sync::Arc::new(CameraFrame::new(4, 4, 33, std::sync::Arc::clone(&buffer)));
let frame_handle = std::sync::Arc::clone(&frame);
preview.set_frame_shared(frame_handle).expect("a matching shared frame is accepted");
assert_eq!(seen.load(std::sync::atomic::Ordering::SeqCst), 33);
assert!(preview.frame().is_none(), "a capture waits for an explicit present");
assert!(preview.present_frame(), "the present swaps the waiting frame in");
let stored = preview.frame().expect("the frame was stored");
assert!(
std::sync::Arc::ptr_eq(&stored.data, &buffer),
"the control must store the host's handle, not a copy"
);
assert_eq!(stored.stride, 16);
assert_eq!(stored.format, PixelFormat::Rgba8888);
}
#[test]
fn adopting_a_whole_shared_frame_is_zero_copy() {
let mut preview = CameraPreview::new(Rect::new(0, 0, 320, 240));
let data: std::sync::Arc<[u8]> = std::sync::Arc::from(vec![3u8; 4 * 4 * 4]);
let frame = std::sync::Arc::new(CameraFrame::new(4, 4, 12, std::sync::Arc::clone(&data)));
let handle = std::sync::Arc::clone(&frame);
preview.set_frame_shared(handle).expect("a well-formed shared frame is adopted");
assert_eq!(
std::sync::Arc::strong_count(&frame),
2,
"the control stores the caller's frame handle, not a re-wrapped copy"
);
assert!(preview.present_frame());
let stored = preview.frame().expect("the frame is on screen");
assert!(std::sync::Arc::ptr_eq(&stored.data, &data));
assert_eq!(stored.format, PixelFormat::Rgba8888);
assert_eq!(stored.stride, 16);
}
#[test]
fn presenting_swaps_the_back_slot_and_announces_seconds() {
let mut preview = CameraPreview::new(Rect::new(0, 0, 320, 240));
preview.start_preview();
let presented = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let p = std::sync::Arc::clone(&presented);
preview.frame_presented.connect(move |pts: std::sync::Arc<f64>| {
p.lock().unwrap().push(*pts);
});
let first: std::sync::Arc<[u8]> = std::sync::Arc::from(vec![1u8; 4 * 4 * 4]);
preview
.set_frame_shared(std::sync::Arc::new(CameraFrame::new(4, 4, 250, first)))
.expect("the first frame is accepted");
let second: std::sync::Arc<[u8]> = std::sync::Arc::from(vec![2u8; 4 * 4 * 4]);
preview
.set_frame_shared(std::sync::Arc::new(CameraFrame::new(4, 4, 500, second)))
.expect("the second frame is accepted");
assert!(preview.frame().is_none(), "nothing is on screen before the first present");
assert_eq!(preview.pending_frame().map(|frame| frame.pts_ms), Some(500));
assert!(preview.present_frame());
assert_eq!(preview.frame().map(|frame| frame.pts_ms), Some(500));
assert!(preview.pending_frame().is_none(), "the back slot is consumed by the swap");
assert!(!preview.present_frame(), "a second present has nothing to swap");
assert_eq!(*presented.lock().unwrap(), vec![0.5], "the signal carries seconds");
}
#[test]
fn a_padded_stride_is_honoured() {
let mut preview = CameraPreview::new(Rect::new(0, 0, 320, 240));
let stride = 32u32;
let data: std::sync::Arc<[u8]> = std::sync::Arc::from(vec![0u8; stride as usize * 4]);
preview
.set_frame_shared_with_format(4, 4, 0, PixelFormat::Rgba8888, stride, data)
.expect("a padded row stride is a valid layout");
assert!(preview.present_frame());
assert_eq!(preview.frame().map(|frame| frame.stride), Some(stride));
}
#[test]
fn enumeration_and_format_negotiation_are_reported() {
let mut preview = CameraPreview::new(Rect::new(0, 0, 320, 240));
let formats = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let f = formats.clone();
preview.format_negotiated.connect(move |value: std::sync::Arc<(u32, u32)>| {
f.lock().unwrap().push(*value);
});
preview.set_available_cameras(vec![CameraDevice::new(1, "Front")]);
assert_eq!(preview.available_cameras().len(), 1);
assert!(preview.set_camera_id(9).is_err(), "an unknown device is refused");
assert_eq!(preview.camera_id(), 0, "a refused switch leaves the selection alone");
preview.set_camera_id(1).expect("the enumerated device is accepted");
assert_eq!(preview.camera_id(), 1);
preview.set_pixel_format(PixelFormat::Bgra8888);
preview.set_target_fps(60);
assert!(!formats.lock().unwrap().is_empty(), "each negotiation is announced");
assert_eq!(
*formats.lock().unwrap().last().expect("a negotiation"),
(640, 480),
"the event carries the negotiated resolution"
);
let data = vec![0u8; 4 * 4 * 4];
assert!(
preview.set_frame(4, 4, &data).is_err(),
"a layout the painter cannot interpret is refused rather than decoded as RGBA"
);
}
#[test]
fn camera_preview_default_state() {
let cp = CameraPreview::new(Rect::new(0, 0, 320, 240));
assert!(!cp.is_active());
assert_eq!(cp.camera_id(), 0);
assert_eq!(cp.resolution(), (640, 480));
assert!(!cp.is_mirror_mode());
assert!(cp.controls_visible());
assert!((cp.zoom_level() - 1.0).abs() < f32::EPSILON);
assert_eq!(cp.kind(), WidgetKind::CameraPreview);
}
#[test]
fn camera_preview_toggle() {
let mut cp = CameraPreview::new(Rect::new(0, 0, 320, 240));
assert!(!cp.is_active());
cp.start_preview();
assert!(cp.is_active());
cp.stop_preview();
assert!(!cp.is_active());
cp.toggle_preview();
assert!(cp.is_active());
cp.toggle_preview();
assert!(!cp.is_active());
}
#[test]
fn camera_preview_zoom() {
let mut cp = CameraPreview::new(Rect::new(0, 0, 320, 240));
assert!((cp.zoom_level() - 1.0).abs() < f32::EPSILON);
cp.zoom_in();
assert!((cp.zoom_level() - 1.5).abs() < f32::EPSILON);
cp.zoom_in();
assert!((cp.zoom_level() - 2.0).abs() < f32::EPSILON);
cp.zoom_out();
assert!((cp.zoom_level() - 1.5).abs() < f32::EPSILON);
cp.set_zoom(5.0);
assert!((cp.zoom_level() - 5.0).abs() < f32::EPSILON);
}
#[test]
fn camera_preview_camera_id_and_resolution() {
let mut cp = CameraPreview::new(Rect::new(0, 0, 320, 240));
cp.set_camera_id(2).expect("with no enumeration any id is a label");
assert_eq!(cp.camera_id(), 2);
cp.set_resolution(1920, 1080);
assert_eq!(cp.resolution(), (1920, 1080));
}
#[test]
fn camera_preview_mirror_and_controls() {
let mut cp = CameraPreview::new(Rect::new(0, 0, 320, 240));
assert!(!cp.is_mirror_mode());
cp.set_mirror_mode(true);
assert!(cp.is_mirror_mode());
assert!(cp.controls_visible());
cp.hide_controls();
assert!(!cp.controls_visible());
cp.show_controls();
assert!(cp.controls_visible());
}
#[test]
fn camera_preview_click_toggles() {
let mut cp = CameraPreview::new(Rect::new(0, 0, 320, 240));
assert!(!cp.is_active());
cp.handle_event(&Event::MousePress { pos: Point::new(10, 10), button: 1, modifiers: 0 });
assert!(cp.is_active());
cp.handle_event(&Event::MousePress { pos: Point::new(10, 10), button: 1, modifiers: 0 });
assert!(!cp.is_active());
}
#[test]
fn setting_the_camera_id_repaints() {
let mut preview = CameraPreview::new(Rect::new(0, 0, 200, 200));
let seen = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
preview.base.redraw_requested.connect({
let seen = std::sync::Arc::clone(&seen);
move || {
seen.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
});
preview.set_camera_id(4).expect("no enumeration means the id is a label");
assert_eq!(
seen.load(std::sync::atomic::Ordering::SeqCst),
1,
"the caption names the device, so it is painted"
);
preview.set_camera_id(4).expect("the same label");
assert_eq!(
seen.load(std::sync::atomic::Ordering::SeqCst),
1,
"the same id is not a change"
);
}
#[test]
fn a_host_frame_negotiates_the_resolution_and_paints() {
let mut preview = CameraPreview::new(Rect::new(0, 0, 320, 240));
preview.start_preview();
let data = vec![200u8; 4 * 4 * 4];
preview.set_frame(4, 4, &data).expect("a matching byte count is accepted");
assert_eq!(preview.resolution(), (4, 4));
assert!(preview.present_frame(), "the frame is swapped on screen");
assert_eq!(preview.frame().map(|frame| frame.width), Some(4));
let svg = crate::widget::svg::render_to_svg(&mut preview);
assert!(svg.starts_with("<svg"));
}
#[test]
fn a_malformed_frame_is_refused_rather_than_painted() {
let mut preview = CameraPreview::new(Rect::new(0, 0, 320, 240));
assert!(preview.set_frame(2, 2, &[0u8; 8]).is_err());
assert!(preview.frame().is_none());
}
#[test]
fn permission_is_reported_as_its_own_state() {
let mut preview = CameraPreview::new(Rect::new(0, 0, 320, 240));
let denied = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let d = denied.clone();
preview.permission_denied.connect(move || {
d.store(true, std::sync::atomic::Ordering::SeqCst);
});
preview.report_permission_denied();
assert!(denied.load(std::sync::atomic::Ordering::SeqCst));
}
}