use std::cell::RefCell;
use denise::{BufferAge, Color, Frame, InputEvent, PixelFormat, Point, Rect, Size, theme};
use denise_render::Pen;
use denise_ui::widget::{Event, EventCtx, Handled, PaintCtx, Widget};
use denise_ui::widgets::Panel;
use denise_ui::{NodeId, Ui};
const SIZE: Size = Size::new(200, 120);
const BOUNDS: Rect = Rect::new(20, 10, 160, 70);
const BAND: i32 = 6;
#[derive(Clone, Debug, PartialEq, Eq)]
enum Msg {}
struct Ledger {
top: i32,
painted: RefCell<Vec<Rect>>,
also_invalidates: bool,
}
impl Ledger {
fn rows(bounds: Rect) -> Rect {
Rect::new(bounds.x, bounds.y, bounds.width, bounds.height - BAND)
}
}
impl Widget<Msg> for Ledger {
fn accepts_pointer(&self) -> bool {
true
}
fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
self.painted.borrow_mut().push(canvas.clip());
let rows = Self::rows(ctx.bounds);
for y in 0..rows.height {
let content = (self.top + y) as u8;
canvas.fill_rect(
Rect::new(rows.x, rows.y + y, rows.width, 1),
Color::rgb(content, 0, 0),
);
}
canvas.fill_rect(
Rect::new(rows.x, rows.bottom(), rows.width, BAND),
Color::rgb(0, 255, 0),
);
}
fn on_event(&mut self, event: &Event<'_>, ctx: &mut EventCtx<'_, Msg>) -> Handled {
let Event::Input(InputEvent::PointerScroll { delta_y, .. }) = event else {
return Handled::No;
};
let dy = *delta_y as i32;
self.top += dy;
ctx.scrolled(Self::rows(ctx.bounds), Point::new(0, dy));
if self.also_invalidates {
ctx.invalidate();
}
Handled::Yes
}
}
struct Fixture {
ui: Ui<Msg>,
ledger: NodeId,
pixels: Vec<u32>,
}
impl Fixture {
fn new(also_invalidates: bool) -> Self {
let mut ui: Ui<Msg> = Ui::new(SIZE, theme::DARK);
let root = ui.root();
let ledger = ui
.add(
root,
Ledger {
top: 100,
painted: RefCell::new(Vec::new()),
also_invalidates,
},
BOUNDS,
)
.expect("ledger");
ui.show_cursor(false);
ui.handle(&[InputEvent::PointerMoved {
position: Point::new(BOUNDS.x + 5, BOUNDS.y + 5),
}]);
let mut fixture = Self {
ui,
ledger,
pixels: vec![0; (SIZE.width * SIZE.height) as usize],
};
fixture.paint(BufferAge::Undefined);
fixture
}
fn paint(&mut self, age: BufferAge) -> Vec<Rect> {
let mut pixels = std::mem::take(&mut self.pixels);
let painted = self.paint_in(&mut pixels, age);
self.pixels = pixels;
painted
}
fn paint_in(&mut self, pixels: &mut [u32], age: BufferAge) -> Vec<Rect> {
let mut frame =
Frame::new(pixels, SIZE, SIZE.width, PixelFormat::Xrgb8888, age).expect("frame");
self.ui.paint(&mut frame);
drop(frame);
self.ui.presented();
let ledger = self.ui.widget::<Ledger>(self.ledger).expect("ledger");
ledger.painted.borrow_mut().drain(..).collect()
}
fn scroll(&mut self, dy: f32) {
self.ui.handle(&[InputEvent::PointerScroll {
delta_x: 0.0,
delta_y: dy,
position: Point::new(BOUNDS.x + 5, BOUNDS.y + 5),
}]);
}
fn red_at(&self, x: i32, y: i32) -> u32 {
(self.pixels[(y * SIZE.width as i32 + x) as usize] >> 16) & 0xFF
}
fn green_at(&self, x: i32, y: i32) -> u32 {
(self.pixels[(y * SIZE.width as i32 + x) as usize] >> 8) & 0xFF
}
fn assert_rows_are(&self, top: i32) {
let rows = Ledger::rows(BOUNDS);
for y in 0..rows.height {
assert_eq!(
self.red_at(rows.x + 3, rows.y + y),
(top + y) as u32 & 0xFF,
"row {y} of the ledger"
);
}
assert_eq!(
self.green_at(rows.x + 3, rows.bottom() + 1),
255,
"the band"
);
}
}
#[test]
fn a_widget_that_scrolls_itself_paints_only_the_strip_and_what_it_left_out() {
let mut f = Fixture::new(false);
let rows = Ledger::rows(BOUNDS);
f.scroll(3.0);
let painted = f.paint(BufferAge::Frames(1));
let strip = Rect::new(rows.x, rows.bottom() - 3, rows.width, 3);
let band = Rect::new(rows.x, rows.bottom(), rows.width, BAND);
assert_eq!(painted.len(), 2, "painted {painted:?}");
assert!(
painted.contains(&strip),
"painted {painted:?}, wanted {strip:?}"
);
assert!(
painted.contains(&band),
"painted {painted:?}, wanted {band:?}"
);
f.assert_rows_are(103);
f.scroll(-5.0);
let painted = f.paint(BufferAge::Frames(1));
let strip = Rect::new(rows.x, rows.y, rows.width, 5);
assert!(
painted.contains(&strip),
"painted {painted:?}, wanted {strip:?}"
);
assert_eq!(painted.len(), 2, "painted {painted:?}");
f.assert_rows_are(98);
}
#[test]
fn moves_accumulate_across_events_and_frames() {
let mut f = Fixture::new(false);
let mut other = f.pixels.clone();
f.scroll(2.0);
f.scroll(3.0);
let painted = f.paint(BufferAge::Frames(1));
assert_eq!(painted.len(), 2, "painted {painted:?}");
f.assert_rows_are(105);
f.scroll(4.0);
let painted = f.paint_in(&mut other, BufferAge::Frames(2));
let rows = Ledger::rows(BOUNDS);
let strip = Rect::new(rows.x, rows.bottom() - 9, rows.width, 9);
assert!(
painted.contains(&strip),
"painted {painted:?}, wanted {strip:?}"
);
std::mem::swap(&mut f.pixels, &mut other);
f.assert_rows_are(109);
}
#[test]
fn the_damage_reported_is_still_the_whole_widget() {
let mut f = Fixture::new(false);
f.scroll(3.0);
let mut frame = Frame::new(
&mut f.pixels,
SIZE,
SIZE.width,
PixelFormat::Xrgb8888,
BufferAge::Frames(1),
)
.expect("frame");
f.ui.paint(&mut frame);
drop(frame);
let damage = f.ui.damage().to_vec();
assert_eq!(damage, vec![BOUNDS], "damage {damage:?}");
}
#[test]
fn invalidating_as_well_takes_the_move_back() {
let mut f = Fixture::new(true);
f.scroll(3.0);
let painted = f.paint(BufferAge::Frames(1));
assert_eq!(painted, vec![BOUNDS], "painted {painted:?}");
f.assert_rows_are(103);
}
#[test]
fn a_node_painted_over_the_rows_stops_the_move() {
let mut f = Fixture::new(false);
let root = f.ui.root();
f.ui.add(root, Panel::default(), Rect::new(60, 20, 80, 24))
.expect("bar");
f.paint(BufferAge::Undefined);
f.scroll(3.0);
let painted = f.paint(BufferAge::Frames(1));
assert_eq!(painted, vec![BOUNDS], "painted {painted:?}");
f.assert_rows_are(103);
}
#[test]
fn other_damage_in_the_frame_means_a_repaint_of_the_rows() {
let mut f = Fixture::new(false);
let root = f.ui.root();
let label =
f.ui.add(root, Panel::default(), Rect::new(0, 100, 200, 20))
.expect("label");
f.paint(BufferAge::Undefined);
f.ui.invalidate(label);
f.scroll(3.0);
let painted = f.paint(BufferAge::Frames(1));
assert_eq!(painted, vec![BOUNDS], "painted {painted:?}");
f.assert_rows_are(103);
}
#[test]
fn a_painter_that_cannot_move_rows_gets_the_whole_viewport() {
use denise::{Mask, Paint, Painter, PixelView};
struct Tally {
clip: Rect,
painted: Vec<Rect>,
}
impl Painter for Tally {
fn size(&self) -> Size {
SIZE
}
fn format(&self) -> PixelFormat {
PixelFormat::Xrgb8888
}
fn clip(&self) -> Rect {
self.clip
}
fn push_clip(&mut self, rect: Rect) -> denise::ClipToken {
let previous = self.clip;
self.clip = self.clip.intersect(&rect).unwrap_or(Rect::ZERO);
denise::ClipToken::restoring(previous)
}
fn pop_clip(&mut self, token: denise::ClipToken) {
self.clip = token.previous();
}
fn clear(&mut self, _: Color) {}
fn fill_rect(&mut self, rect: Rect, _: Paint) {
if let Some(visible) = rect.intersect(&self.clip) {
self.painted.push(visible);
}
}
fn fill_rounded_rect(&mut self, _: Rect, _: i32, _: Paint) {}
fn stroke_rounded_rect(&mut self, _: Rect, _: i32, _: i32, _: Paint) {}
fn fill_circle(&mut self, _: Point, _: i32, _: Paint) {}
fn stroke_circle(&mut self, _: Point, _: i32, _: i32, _: Paint) {}
fn stroke_arc(&mut self, _: Point, _: i32, _: i32, _: i32, _: i32, _: Paint) {}
fn draw_line(&mut self, _: Point, _: Point, _: Paint) {}
fn fill_polygon_fx(&mut self, _: &[(i32, i32)], _: Paint) {}
fn blit_mask(&mut self, _: Point, _: &Mask<'_>, _: Paint) {}
fn blit(&mut self, _: &PixelView<'_>, _: Point) {}
fn blit_scaled(&mut self, _: &PixelView<'_>, _: Rect) {}
fn blit_rounded(&mut self, _: &PixelView<'_>, _: Rect, _: Rect, _: i32) {}
}
let mut f = Fixture::new(false);
f.scroll(3.0);
let mut tally = Tally {
clip: Rect::from_size(SIZE),
painted: Vec::new(),
};
f.ui.paint_with(&mut Pen::new(&mut tally), BufferAge::Frames(1));
let rows = Ledger::rows(BOUNDS);
let filled: Vec<&Rect> = tally.painted.iter().filter(|r| r.height == 1).collect();
assert_eq!(filled.len(), rows.height as usize, "filled {filled:?}");
}