#![cfg(not(alloc_frugal))]
use rust_widgets::core::{Color, ObjectId, Rect, Size};
use rust_widgets::event::EventHandler;
use rust_widgets::render::{PaintBackend, SoftwarePaintBackend};
use rust_widgets::widget::runtime::{self, repaint_mode, set_repaint_mode, RepaintMode};
use rust_widgets::widget::{Draw, Widget};
struct PaintedPanel {
base: rust_widgets::widget::BaseWidget,
children: Vec<ObjectId>,
}
impl PaintedPanel {
fn new(geometry: Rect, children: Vec<ObjectId>) -> Self {
let mut base = rust_widgets::widget::BaseWidget::new(
rust_widgets::widget::WidgetKind::Panel,
geometry,
"PaintedPanel",
);
for child in &children {
base.add_child(*child);
}
Self { base, children }
}
}
impl Widget for PaintedPanel {
fn base(&self) -> &rust_widgets::widget::BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut rust_widgets::widget::BaseWidget {
&mut self.base
}
fn as_draw_mut(&mut self) -> Option<&mut dyn Draw> {
Some(self)
}
}
impl EventHandler for PaintedPanel {
fn handle_event(&mut self, event: &rust_widgets::event::Event) {
self.base.handle_event(event);
}
}
impl Draw for PaintedPanel {
fn draw(&mut self, ctx: &mut rust_widgets::render::RenderContext) {
let rect = self.base.geometry();
ctx.fill_rect(rect, Color::rgb(40, 60, 90));
for (index, _) in self.children.iter().enumerate() {
let x = (index as i32 % 8) * 80;
let y = (index as i32 / 8) * 60;
ctx.fill_rect(
Rect::new(rect.x + x + 4, rect.y + y + 4, 72, 52),
Color::rgb(200, 180, 90),
);
}
}
}
fn window_with_children(size: Size, count: usize) -> ObjectId {
let rect = Rect::new(0, 0, size.width, size.height);
let children: Vec<ObjectId> = (0..count).map(|index| index as ObjectId + 1).collect();
let panel = PaintedPanel::new(rect, children);
panel.base.request_redraw();
let id = runtime::register(Box::new(panel)).expect("registry");
assert!(runtime::set_geometry(id, rect));
id
}
fn time_frames(id: ObjectId, size: Size, mode: RepaintMode, frames: u32) -> (u128, u32) {
assert!(set_repaint_mode(id, mode));
let mut previous = runtime::render_frame_incremental(id, size, Color::BLACK, None);
let mut regioned = 0;
let start = std::time::Instant::now();
for _ in 1..frames {
runtime::mark_dirty_rect(id, Rect::new(4, 4, 72, 52));
runtime::with_widget(id, |widget| widget.base().request_redraw()).expect("widget");
if !runtime::dirty_rects(id).is_empty() {
regioned += 1;
}
let frame_bytes =
runtime::render_frame_incremental(id, size, Color::BLACK, previous.as_deref())
.expect("frame");
previous = Some(frame_bytes);
}
(start.elapsed().as_micros(), regioned)
}
#[test]
fn a_partial_repaint_is_cheaper_than_a_full_one() {
let size = Size::new(1280, 960);
let frames = 40;
let full_id = window_with_children(size, 32);
let (full, _) = time_frames(full_id, size, RepaintMode::Full, frames);
let dirty_id = window_with_children(size, 32);
let (dirty, regioned) = time_frames(dirty_id, size, RepaintMode::Dirty, frames);
let ratio = dirty as f64 / full as f64;
println!("full={full}us dirty={dirty}us ratio={ratio:.3} regioned_frames={regioned}");
assert!(
dirty < full,
"a regioned repaint must beat a full one: dirty={dirty}us full={full}us. \
If this fails the auto-decision is enabling pure bookkeeping."
);
runtime::unregister(full_id);
runtime::unregister(dirty_id);
}
#[test]
fn a_partial_repaint_actually_regions() {
let size = Size::new(1280, 960);
let dirty_id = window_with_children(size, 32);
let (_, regioned) = time_frames(dirty_id, size, RepaintMode::Dirty, 20);
assert_eq!(regioned, 19, "every frame with recorded damage must reach the regioned path");
runtime::unregister(dirty_id);
let full_id = window_with_children(size, 32);
assert!(set_repaint_mode(full_id, RepaintMode::Full));
let recorded = runtime::mark_dirty_rect(full_id, Rect::new(4, 4, 72, 52));
assert!(!recorded, "Full mode must refuse damage it will never read");
assert!(runtime::dirty_rects(full_id).is_empty(), "and must not have tracked it");
runtime::unregister(full_id);
}
#[test]
fn adaptive_is_not_meaningfully_slower_than_dirty() {
let size = Size::new(1280, 960);
let frames = 40;
let dirty_id = window_with_children(size, 32);
let (dirty, _) = time_frames(dirty_id, size, RepaintMode::Dirty, frames);
let adaptive_id = window_with_children(size, 32);
let (adaptive, _) = time_frames(adaptive_id, size, RepaintMode::Adaptive, frames);
let ratio = adaptive as f64 / dirty as f64;
println!("dirty={dirty}us adaptive={adaptive}us ratio={ratio:.3}");
assert!(
ratio < 3.0,
"Adaptive must stay close to Dirty: adaptive={adaptive}us dirty={dirty}us \
(ratio {ratio:.3}). Adaptive pays one coverage measurement per frame; if that \
dominates, Dirty is the better default."
);
runtime::unregister(dirty_id);
runtime::unregister(adaptive_id);
}
#[test]
fn the_threshold_is_a_honest_place_to_refuse() {
let size = Size::new(400, 300);
let frames = 40;
let full_id = window_with_children(size, 4);
let (full, _) = time_frames(full_id, size, RepaintMode::Full, frames);
let dirty_id = window_with_children(size, 4);
let (dirty, _) = time_frames(dirty_id, size, RepaintMode::Dirty, frames);
println!("small surface: full={full}us dirty={dirty}us");
if full > 0 {
println!("saving: {:.1}%", (1.0 - dirty as f64 / full as f64) * 100.0);
}
assert!(full > 0);
assert!(dirty > 0);
runtime::unregister(full_id);
runtime::unregister(dirty_id);
}
#[test]
fn the_auto_decision_selects_adaptive_where_it_accepts() {
let large = window_with_children(Size::new(1280, 960), 8);
assert_eq!(repaint_mode(large), RepaintMode::Adaptive);
let small = window_with_children(Size::new(320, 240), 4);
assert_eq!(repaint_mode(small), RepaintMode::Full);
runtime::unregister(large);
runtime::unregister(small);
}
#[test]
fn the_software_backend_is_reachable_for_custom_paint() {
let mut backend = SoftwarePaintBackend::new(Size::new(8, 8), 1.0);
backend.begin_frame(Color::BLACK);
backend.end_frame();
assert_eq!(backend.frame_rgba().len(), 8 * 8 * 4);
}