use crate::core::{Color, HorizontalAlignment, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::capability::coercion::{expect_bool, expect_string, expect_usize};
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::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
use std::time::Instant;
pub trait WidgetAndDraw: Widget {
fn draw_widget(&mut self, context: &mut RenderContext);
}
impl<T: Widget + Draw> WidgetAndDraw for T {
fn draw_widget(&mut self, context: &mut RenderContext) {
self.draw(context);
}
}
pub struct CarouselPage {
pub title: String,
pub color: Color,
content: Option<Box<dyn WidgetAndDraw>>,
}
impl CarouselPage {
pub fn new(title: impl Into<String>, color: Color) -> Self {
Self { title: title.into(), color, content: None }
}
pub fn has_content(&self) -> bool {
self.content.is_some()
}
pub fn content(&self) -> Option<&dyn WidgetAndDraw> {
self.content.as_deref().map(|content| content as &dyn WidgetAndDraw)
}
pub fn content_mut(&mut self) -> Option<&mut dyn WidgetAndDraw> {
match self.content.as_deref_mut() {
Some(content) => Some(content),
None => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CarouselIndicatorStyle {
#[default]
Dots,
Bars,
Numeric,
Hidden,
}
impl CarouselIndicatorStyle {
pub fn as_str(self) -> &'static str {
match self {
CarouselIndicatorStyle::Dots => "dots",
CarouselIndicatorStyle::Bars => "bars",
CarouselIndicatorStyle::Numeric => "numeric",
CarouselIndicatorStyle::Hidden => "hidden",
}
}
pub fn from_name(name: &str) -> Option<Self> {
Some(match name {
"dots" => CarouselIndicatorStyle::Dots,
"bars" => CarouselIndicatorStyle::Bars,
"numeric" => CarouselIndicatorStyle::Numeric,
"hidden" => CarouselIndicatorStyle::Hidden,
_ => return None,
})
}
pub fn is_visible(self) -> bool {
!matches!(self, CarouselIndicatorStyle::Hidden)
}
pub fn is_per_page(self) -> bool {
matches!(self, CarouselIndicatorStyle::Dots | CarouselIndicatorStyle::Bars)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CarouselIndicatorPosition {
#[default]
Bottom,
Top,
Left,
Right,
}
impl CarouselIndicatorPosition {
pub fn as_str(self) -> &'static str {
match self {
CarouselIndicatorPosition::Bottom => "bottom",
CarouselIndicatorPosition::Top => "top",
CarouselIndicatorPosition::Left => "left",
CarouselIndicatorPosition::Right => "right",
}
}
pub fn from_name(name: &str) -> Option<Self> {
Some(match name {
"bottom" => CarouselIndicatorPosition::Bottom,
"top" => CarouselIndicatorPosition::Top,
"left" => CarouselIndicatorPosition::Left,
"right" => CarouselIndicatorPosition::Right,
_ => return None,
})
}
pub fn is_vertical(self) -> bool {
matches!(self, CarouselIndicatorPosition::Left | CarouselIndicatorPosition::Right)
}
}
const SWIPE_THRESHOLD_FRACTION: f32 = 0.18;
const FLICK_VELOCITY_PX_PER_SEC: f32 = 400.0;
const MIN_FLICK_FRACTION: f32 = 0.02;
const INDICATOR_STRIP: u32 = 26;
#[derive(Debug, Clone, Copy, PartialEq)]
enum DragState {
Idle,
Pressed { start_x: i32 },
Swiping { start_x: i32, offset_x: i32, last_x: i32, last_moved_at: Option<Instant> },
}
pub struct Carousel {
base: BaseWidget,
pages: Vec<CarouselPage>,
current_index: usize,
r#loop: bool,
autoplay_interval_ms: Option<u64>,
autoplay_elapsed_ms: u64,
pointer_inside: bool,
indicator_style: CarouselIndicatorStyle,
indicator_position: CarouselIndicatorPosition,
drag: DragState,
pub page_changed: Signal1<usize>,
}
impl Carousel {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Carousel, geometry, "Carousel"),
pages: Vec::new(),
current_index: 0,
r#loop: false,
autoplay_interval_ms: None,
autoplay_elapsed_ms: 0,
pointer_inside: false,
indicator_style: CarouselIndicatorStyle::Dots,
indicator_position: CarouselIndicatorPosition::Bottom,
drag: DragState::Idle,
page_changed: Signal1::new(),
}
}
pub fn add_page(&mut self, title: impl Into<String>, color: Color) -> usize {
let index = self.pages.len();
self.pages.push(CarouselPage::new(title, color));
self.base.request_redraw();
index
}
pub fn set_page_content(&mut self, index: usize, content: Box<dyn WidgetAndDraw>) -> bool {
match self.pages.get_mut(index) {
Some(page) => {
page.content = Some(content);
self.base.request_redraw();
true
}
None => false,
}
}
pub fn remove_page(&mut self, index: usize) -> Option<CarouselPage> {
if index >= self.pages.len() {
return None;
}
let removed = self.pages.remove(index);
let last = self.pages.len().saturating_sub(1);
if self.current_index > last {
self.current_index = last;
self.page_changed.emit(self.current_index);
}
self.base.request_redraw();
Some(removed)
}
pub fn set_current(&mut self, index: usize) {
let clamped = index.min(self.pages.len().saturating_sub(1));
if self.current_index != clamped {
self.current_index = clamped;
self.autoplay_elapsed_ms = 0;
self.page_changed.emit(clamped);
self.base.request_redraw();
}
}
pub fn current(&self) -> usize {
self.current_index
}
pub fn page_count(&self) -> usize {
self.pages.len()
}
pub fn next(&mut self) {
if self.pages.len() < 2 {
return;
}
if self.current_index + 1 < self.pages.len() {
self.set_current(self.current_index + 1);
} else if self.r#loop {
self.set_current(0);
}
}
pub fn previous(&mut self) {
if self.pages.len() < 2 {
return;
}
if self.current_index > 0 {
self.set_current(self.current_index - 1);
} else if self.r#loop {
self.set_current(self.pages.len() - 1);
}
}
pub fn set_loop(&mut self, loop_enabled: bool) {
self.r#loop = loop_enabled;
self.base.request_redraw();
}
pub fn r#loop(&self) -> bool {
self.r#loop
}
pub fn set_autoplay(&mut self, interval: Option<core::time::Duration>) {
self.autoplay_interval_ms = match interval {
Some(duration) => {
let ms = duration.as_millis() as u64;
if ms == 0 {
None
} else {
Some(ms)
}
}
None => None,
};
self.autoplay_elapsed_ms = 0;
self.base.request_redraw();
}
pub fn autoplay(&self) -> Option<core::time::Duration> {
self.autoplay_interval_ms.map(core::time::Duration::from_millis)
}
pub fn set_indicator_style(&mut self, style: CarouselIndicatorStyle) {
self.indicator_style = style;
self.base.request_redraw();
}
pub fn indicator_style(&self) -> CarouselIndicatorStyle {
self.indicator_style
}
pub fn set_indicator_position(&mut self, position: CarouselIndicatorPosition) {
self.indicator_position = position;
self.base.request_redraw();
}
pub fn indicator_position(&self) -> CarouselIndicatorPosition {
self.indicator_position
}
pub fn current_page(&self) -> Option<&CarouselPage> {
self.pages.get(self.current_index)
}
pub fn current_page_mut(&mut self) -> Option<&mut CarouselPage> {
self.pages.get_mut(self.current_index)
}
pub fn pages(&self) -> &[CarouselPage] {
&self.pages
}
pub fn current_page_title(&self) -> &str {
self.current_page().map_or("", |page| page.title.as_str())
}
pub fn content_rect(&self) -> Rect {
let rect = self.geometry();
if !self.indicator_drawn() {
return rect;
}
match self.indicator_position {
CarouselIndicatorPosition::Bottom => {
Rect::new(rect.x, rect.y, rect.width, rect.height.saturating_sub(INDICATOR_STRIP))
}
CarouselIndicatorPosition::Top => Rect::new(
rect.x,
rect.y + INDICATOR_STRIP as i32,
rect.width,
rect.height.saturating_sub(INDICATOR_STRIP),
),
CarouselIndicatorPosition::Left => Rect::new(
rect.x + INDICATOR_STRIP as i32,
rect.y,
rect.width.saturating_sub(INDICATOR_STRIP),
rect.height,
),
CarouselIndicatorPosition::Right => {
Rect::new(rect.x, rect.y, rect.width.saturating_sub(INDICATOR_STRIP), rect.height)
}
}
}
fn indicator_drawn(&self) -> bool {
self.indicator_style.is_visible() && self.pages.len() > 1
}
fn autoplay_should_run(&self) -> bool {
self.autoplay_interval_ms.is_some()
&& self.pages.len() > 1
&& !self.pointer_inside
&& self.drag == DragState::Idle
&& self.base.is_enabled()
&& self.base.is_visible()
}
fn swipe_threshold_px(&self) -> i32 {
let width = self.geometry().width as f32;
(width * SWIPE_THRESHOLD_FRACTION).max(1.0) as i32
}
}
fn swipe_velocity_px_per_sec(
last_x: i32,
release_x: i32,
last_moved_at: Option<Instant>,
now: Instant,
width: f32,
) -> f32 {
let Some(last_moved_at) = last_moved_at else {
return 0.0;
};
let Some(elapsed) = now.checked_duration_since(last_moved_at) else {
return 0.0;
};
let seconds = elapsed.as_secs_f32();
if seconds <= 0.0 {
return 0.0;
}
let travel = (release_x - last_x) as f32;
let cap = width.abs().max(1.0) * 10.0;
(travel / seconds).clamp(-cap, cap)
}
impl Widget for Carousel {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
crate::core::Size::new(300, 200)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for Carousel {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"current_index" => Ok(CapabilityValue::UInt(self.current() as u64)),
"item_count" => Ok(CapabilityValue::UInt(self.page_count() as u64)),
"current_page_title" => {
Ok(CapabilityValue::String(self.current_page_title().to_string()))
}
"loop" => Ok(CapabilityValue::Bool(self.r#loop())),
"autoplay_interval" => Ok(match self.autoplay_interval_ms {
Some(ms) => CapabilityValue::UInt(ms),
None => CapabilityValue::Null,
}),
"indicator_style" => {
Ok(CapabilityValue::String(self.indicator_style.as_str().to_string()))
}
"indicator_position" => {
Ok(CapabilityValue::String(self.indicator_position.as_str().to_string()))
}
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"current_index" => {
self.set_current(expect_usize(value)?);
Ok(())
}
"loop" => {
self.set_loop(expect_bool(value)?);
Ok(())
}
"autoplay_interval" => match value {
CapabilityValue::Null => {
self.set_autoplay(None);
Ok(())
}
other => {
let ms = expect_usize(other)? as u64;
self.set_autoplay(Some(core::time::Duration::from_millis(ms)));
Ok(())
}
},
"indicator_style" => {
let text = expect_string(value)?;
let Some(style) = CarouselIndicatorStyle::from_name(&text) else {
return Err(CapabilityAccessError::TypeMismatch);
};
self.set_indicator_style(style);
Ok(())
}
"indicator_position" => {
let text = expect_string(value)?;
let Some(position) = CarouselIndicatorPosition::from_name(&text) else {
return Err(CapabilityAccessError::TypeMismatch);
};
self.set_indicator_position(position);
Ok(())
}
"item_count" | "current_page_title" => Err(CapabilityAccessError::ReadOnlyProperty),
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"current_index",
"item_count",
"current_page_title",
"loop",
"autoplay_interval",
"indicator_style",
"indicator_position",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for Carousel {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let is_enabled = self.base.is_enabled();
let page_count = self.pages.len();
if page_count == 0 {
context.fill_rounded_rect(rect, 8, Color::rgba(230, 230, 230, 200));
return;
}
let content_rect = self.content_rect();
let (offset, neighbour) = match self.drag {
DragState::Swiping { offset_x, .. } => {
let target = self.neighbour_for_offset(offset_x);
(offset_x, target)
}
_ => (0, None),
};
if let Some(neighbour_index) = neighbour {
let neighbour_x = (content_rect.x as f32
+ offset as f32
+ if offset > 0 { -(rect.width as f32) } else { rect.width as f32 })
as i32;
let neighbour_rect =
Rect::new(neighbour_x, content_rect.y, content_rect.width, content_rect.height);
self.draw_page(context, neighbour_index, neighbour_rect, is_enabled);
}
let current_rect = Rect::new(
content_rect.x + offset,
content_rect.y,
content_rect.width,
content_rect.height,
);
self.draw_page(context, self.current_index, current_rect, is_enabled);
if self.indicator_drawn() {
self.draw_indicator(context, rect);
}
}
}
impl Carousel {
fn neighbour_for_offset(&self, offset_x: i32) -> Option<usize> {
if offset_x == 0 || self.swipe_direction(offset_x).is_none() {
return None;
}
self.step_for_offset(offset_x)
}
fn swipe_direction(&self, offset_x: i32) -> Option<i32> {
if offset_x.abs() < self.swipe_threshold_px() {
return None;
}
Some(if offset_x > 0 { -1 } else { 1 })
}
fn swipe_direction_at(&self, offset_x: i32, velocity_px_per_sec: f32) -> Option<i32> {
if let Some(step) = self.swipe_direction(offset_x) {
return Some(step);
}
let floor_px = self.min_flick_px();
if velocity_px_per_sec.abs() >= FLICK_VELOCITY_PX_PER_SEC && offset_x.abs() >= floor_px {
return Some(if velocity_px_per_sec > 0.0 { -1 } else { 1 });
}
None
}
fn min_flick_px(&self) -> i32 {
(self.geometry().width as f32 * MIN_FLICK_FRACTION).max(1.0) as i32
}
fn wrapped_step(&self, step: i32) -> Option<usize> {
if self.pages.len() < 2 {
return None;
}
let count = self.pages.len() as i32;
let candidate = self.current_index as i32 + step;
if candidate < 0 {
return if self.r#loop { Some((count - 1) as usize) } else { None };
}
if candidate >= count {
return if self.r#loop { Some(0) } else { None };
}
Some(candidate as usize)
}
fn step_for_offset(&self, offset_x: i32) -> Option<usize> {
let step = if offset_x > 0 { -1 } else { 1 };
self.wrapped_step(step)
}
fn draw_page(
&mut self,
context: &mut RenderContext,
index: usize,
page_rect: Rect,
is_enabled: bool,
) {
let (color, title, has_content) = {
let Some(page) = self.pages.get(index) else {
return;
};
(page.color, page.title.clone(), page.content.is_some())
};
let bg_color = if !is_enabled {
Color::rgba(
color.r.saturating_sub(40),
color.g.saturating_sub(40),
color.b.saturating_sub(40),
160,
)
} else {
color
};
context.fill_rounded_rect(page_rect, 8, bg_color);
if has_content {
if let Some(page) = self.pages.get_mut(index) {
if let Some(content) = page.content_mut() {
content.set_geometry(page_rect);
content.set_enabled(is_enabled);
context.push_clip(page_rect.x, page_rect.y, page_rect.width, page_rect.height);
content.draw_widget(context);
context.pop_clip();
}
}
return;
}
let font = crate::core::Font::with_weight("Arial", 18.0, 600, false);
let metrics = context.measure_text(&title, &font);
let text_x = page_rect.x + (page_rect.width as i32 - metrics.width as i32) / 2;
let text_y = page_rect.y + (page_rect.height as i32 / 2) - (metrics.height as i32 / 2)
+ metrics.ascent as i32;
let text_color = if !is_enabled { Color::rgba(255, 255, 255, 160) } else { Color::WHITE };
context.draw_text(
Point::new(text_x, text_y),
&title,
&font,
text_color,
HorizontalAlignment::Left,
);
}
fn draw_indicator(&self, context: &mut RenderContext, rect: Rect) {
const MAX_VISIBLE_SLOTS: usize = 20;
const SIDE_SLOTS: usize = 9;
const SLOT_SPACING: i32 = 16;
const DOT_RADIUS: u32 = 4;
const BAR_WIDTH: u32 = 10;
const BAR_HEIGHT: u32 = 3;
let page_count = self.pages.len();
let vertical = self.indicator_position.is_vertical();
let (strip_center, cross_center) = match self.indicator_position {
CarouselIndicatorPosition::Bottom => (
rect.x + rect.width as i32 / 2,
rect.y + rect.height as i32 - (INDICATOR_STRIP as i32 / 2),
),
CarouselIndicatorPosition::Top => {
(rect.x + rect.width as i32 / 2, rect.y + INDICATOR_STRIP as i32 / 2)
}
CarouselIndicatorPosition::Left => {
(rect.y + rect.height as i32 / 2, rect.x + INDICATOR_STRIP as i32 / 2)
}
CarouselIndicatorPosition::Right => (
rect.y + rect.height as i32 / 2,
rect.x + rect.width as i32 - (INDICATOR_STRIP as i32 / 2),
),
};
if !self.indicator_style.is_per_page() {
let label = format!("{}/{}", self.current_index + 1, page_count);
let font = crate::core::Font::simple("Sans", 12.0);
let metrics = context.measure_text(&label, &font);
let (text_x, text_y) = if vertical {
(
cross_center - metrics.width as i32 / 2,
strip_center - metrics.height as i32 / 2 + metrics.ascent as i32,
)
} else {
(
strip_center - metrics.width as i32 / 2,
cross_center - metrics.height as i32 / 2 + metrics.ascent as i32,
)
};
context.draw_text(
Point::new(text_x, text_y),
&label,
&font,
Color::WHITE,
HorizontalAlignment::Left,
);
return;
}
let slots: Vec<Option<usize>> = if page_count <= MAX_VISIBLE_SLOTS {
(0..page_count).map(Some).collect()
} else {
let mut slots: Vec<Option<usize>> = (0..SIDE_SLOTS).map(Some).collect();
slots.push(None); for i in (page_count - (MAX_VISIBLE_SLOTS - SIDE_SLOTS - 1))..page_count {
slots.push(Some(i));
}
slots
};
let count = slots.len() as i32;
let span = count * SLOT_SPACING;
for (i, slot) in slots.iter().enumerate() {
let along = strip_center - span / 2 + (i as i32) * SLOT_SPACING + SLOT_SPACING / 2;
let active = *slot == Some(self.current_index);
let Some(_) = slot else {
let mark = if vertical {
Rect::new(cross_center - 1, along - 1, 2, 2)
} else {
Rect::new(along - 1, cross_center - 1, 2, 2)
};
context.fill_rounded_rect(mark, 1, Color::rgba(255, 255, 255, 60));
continue;
};
let marker_rect: Rect = match self.indicator_style {
CarouselIndicatorStyle::Dots => {
let size = DOT_RADIUS as i32 * 2;
if vertical {
Rect::new(cross_center, along - DOT_RADIUS as i32, size as u32, size as u32)
} else {
Rect::new(along - DOT_RADIUS as i32, cross_center, size as u32, size as u32)
}
}
CarouselIndicatorStyle::Bars => {
let (w, h) =
if vertical { (BAR_HEIGHT, BAR_WIDTH) } else { (BAR_WIDTH, BAR_HEIGHT) };
Rect::new(cross_center - (w as i32 / 2), along - (h as i32 / 2), w, h)
}
CarouselIndicatorStyle::Numeric | CarouselIndicatorStyle::Hidden => continue,
};
let mark_color = if active { Color::WHITE } else { Color::rgba(255, 255, 255, 120) };
context.fill_rounded_rect(marker_rect, 4, mark_color);
if active {
let halo = Rect::new(
marker_rect.x - 1,
marker_rect.y - 1,
marker_rect.width + 2,
marker_rect.height + 2,
);
context.draw_rounded_rect_stroke(halo, 5, Color::rgba(255, 255, 255, 80), 1);
}
}
}
}
impl EventHandler for Carousel {
fn handle_event(&mut self, event: &Event) {
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos, button } if *button == 1 => {
self.drag = DragState::Pressed { start_x: pos.x };
self.base.handle_event(event);
}
Event::MouseMove { pos } => match self.drag {
DragState::Pressed { start_x } => {
let offset = pos.x - start_x;
if offset.abs() >= self.swipe_threshold_px() {
self.drag = DragState::Swiping {
start_x,
offset_x: offset,
last_x: pos.x,
last_moved_at: Some(Instant::now()),
};
self.base.request_redraw();
}
}
DragState::Swiping { start_x, .. } => {
self.drag = DragState::Swiping {
start_x,
offset_x: pos.x - start_x,
last_x: pos.x,
last_moved_at: Some(Instant::now()),
};
self.base.request_redraw();
}
DragState::Idle => {}
},
Event::MouseRelease { pos, button } if *button == 1 => {
let previous_drag = self.drag;
self.drag = DragState::Idle;
match previous_drag {
DragState::Swiping { offset_x, last_x, last_moved_at, .. } => {
let velocity = swipe_velocity_px_per_sec(
last_x,
pos.x,
last_moved_at,
Instant::now(),
self.geometry().width as f32,
);
match self.swipe_direction_at(offset_x, velocity) {
Some(step) => {
if let Some(target) = self.wrapped_step(step) {
self.set_current(target);
}
}
None => self.page_for_click(pos.x),
}
self.base.request_redraw();
}
DragState::Pressed { .. } | DragState::Idle => {
self.page_for_click(pos.x);
self.base.request_redraw();
}
}
self.base.handle_event(event);
}
#[cfg(feature = "touch")]
Event::Swipe { start, end, .. } => {
let travel = end.x - start.x;
if let Some(step) = self.swipe_direction(travel) {
if let Some(target) = self.wrapped_step(step) {
self.set_current(target);
}
}
}
Event::KeyPress { key, .. } | Event::KeyDown((key, _)) => match *key {
37 => self.previous(), 39 => self.next(), _ => {
self.base.handle_event(event);
}
},
Event::Timer { .. } => {
self.advance_autoplay();
}
Event::MouseEnter { .. } => {
self.pointer_inside = true;
self.base.handle_event(event);
}
Event::MouseLeave { .. } => {
self.pointer_inside = false;
self.base.handle_event(event);
}
_ => {
self.forward_to_current_page(event);
self.base.handle_event(event);
}
}
}
}
impl Carousel {
fn page_for_click(&mut self, x: i32) {
let rect = self.geometry();
let mid_x = rect.x + (rect.width as i32) / 2;
if x < mid_x {
self.previous();
} else {
self.next();
}
}
fn forward_to_current_page(&mut self, event: &Event) {
let content_rect = self.content_rect();
match event {
Event::MousePress { pos, .. }
| Event::MouseRelease { pos, .. }
| Event::MouseMove { pos }
| Event::MouseDoubleClick { pos, .. }
if !content_rect.contains(*pos) =>
{
return;
}
_ => {}
}
if let Some(page) = self.pages.get_mut(self.current_index) {
if let Some(content) = page.content_mut() {
content.set_geometry(content_rect);
content.handle_event(event);
}
}
}
pub fn advance_autoplay(&mut self) {
if !self.autoplay_should_run() {
return;
}
let Some(interval) = self.autoplay_interval_ms else {
return;
};
self.autoplay_elapsed_ms = self.autoplay_elapsed_ms.saturating_add(interval);
if self.autoplay_elapsed_ms < interval {
return;
}
self.autoplay_elapsed_ms = 0;
if self.current_index + 1 < self.pages.len() {
self.set_current(self.current_index + 1);
} else if self.r#loop {
self.set_current(0);
}
}
pub fn advance_autoplay_by(&mut self, elapsed: core::time::Duration) {
if !self.autoplay_should_run() {
return;
}
self.autoplay_elapsed_ms =
self.autoplay_elapsed_ms.saturating_add(elapsed.as_millis() as u64);
let Some(interval) = self.autoplay_interval_ms else {
return;
};
if self.autoplay_elapsed_ms < interval {
return;
}
self.autoplay_elapsed_ms = 0;
if self.current_index + 1 < self.pages.len() {
self.set_current(self.current_index + 1);
} else if self.r#loop {
self.set_current(0);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Point, Size};
use crate::render::{PaintBackend, SoftwarePaintBackend};
use crate::widget::base_widgets::label::Label;
use std::sync::{Arc, Mutex};
fn default_carousel() -> Carousel {
let mut c = Carousel::new(Rect::new(0, 0, 300, 200));
c.add_page("Page 1", Color::rgb(52, 120, 246));
c.add_page("Page 2", Color::rgb(52, 199, 89));
c.add_page("Page 3", Color::rgb(255, 149, 0));
c
}
fn render_rgba(widget: &mut Carousel, size: Size) -> Vec<u8> {
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::WHITE);
let mut context = RenderContext::new(&mut backend);
widget.draw(&mut context);
backend.end_frame();
backend.frame_rgba().to_vec()
}
fn pixel(rgba: &[u8], width: u32, x: u32, y: u32) -> (u8, u8, u8, u8) {
let index = ((y * width + x) * 4) as usize;
(rgba[index], rgba[index + 1], rgba[index + 2], rgba[index + 3])
}
#[test]
fn carousel_creation_defaults() {
let c = Carousel::new(Rect::new(0, 0, 300, 200));
assert_eq!(c.current(), 0);
assert_eq!(c.page_count(), 0);
assert!(c.current_page().is_none());
assert_eq!(c.kind(), WidgetKind::Carousel);
assert!(!c.r#loop());
assert!(c.autoplay().is_none());
assert_eq!(c.indicator_style(), CarouselIndicatorStyle::Dots);
assert_eq!(c.indicator_position(), CarouselIndicatorPosition::Bottom);
}
#[test]
fn carousel_add_pages() {
let mut c = Carousel::new(Rect::new(0, 0, 300, 200));
assert_eq!(c.page_count(), 0);
c.add_page("Intro", Color::rgb(100, 100, 200));
assert_eq!(c.page_count(), 1);
c.add_page("Details", Color::rgb(200, 100, 100));
assert_eq!(c.page_count(), 2);
assert_eq!(c.current(), 0);
assert_eq!(c.current_page().unwrap().title, "Intro");
}
#[test]
fn carousel_navigation() {
let mut c = default_carousel();
assert_eq!(c.current(), 0);
c.next();
assert_eq!(c.current(), 1);
c.next();
assert_eq!(c.current(), 2);
c.next();
assert_eq!(c.current(), 2);
c.previous();
assert_eq!(c.current(), 1);
c.previous();
assert_eq!(c.current(), 0);
c.previous();
assert_eq!(c.current(), 0);
}
#[test]
fn carousel_set_current_clamps() {
let mut c = default_carousel();
c.set_current(5); assert_eq!(c.current(), 2);
}
#[test]
fn carousel_signal_emission() {
let mut c = default_carousel();
let captured = Arc::new(Mutex::new(None));
c.page_changed.connect({
let captured = Arc::clone(&captured);
move |val: Arc<usize>| {
*captured.lock().unwrap() = Some(*val);
}
});
c.next();
assert_eq!(c.current(), 1);
assert_eq!(*captured.lock().unwrap(), Some(1));
c.set_current(1);
assert_eq!(*captured.lock().unwrap(), Some(1));
c.previous();
assert_eq!(*captured.lock().unwrap(), Some(0));
}
#[test]
fn carousel_mouse_press_navigates() {
let mut c = default_carousel();
c.handle_event(&Event::MouseRelease { pos: Point::new(50, 100), button: 1 });
assert_eq!(c.current(), 0);
c.handle_event(&Event::MouseRelease { pos: Point::new(250, 100), button: 1 });
assert_eq!(c.current(), 1);
}
#[test]
fn carousel_disabled_blocks_events() {
let mut c = default_carousel();
c.set_enabled(false);
c.handle_event(&Event::MouseRelease { pos: Point::new(250, 100), button: 1 });
assert_eq!(c.current(), 0, "a disabled carousel must not page on release");
c.handle_event(&Event::KeyPress { key: 39, modifiers: 0 });
assert_eq!(c.current(), 0, "a disabled carousel must not page on right arrow");
c.handle_event(&Event::MousePress { pos: Point::new(280, 100), button: 1 });
c.handle_event(&Event::MouseMove { pos: Point::new(200, 100) });
c.handle_event(&Event::MouseRelease { pos: Point::new(200, 100), button: 1 });
assert_eq!(c.current(), 0, "a disabled carousel must not page on a swipe");
}
#[test]
fn carousel_add_page_returns_index() {
let mut c = Carousel::new(Rect::new(0, 0, 300, 200));
let idx0 = c.add_page("A", Color::RED);
let idx1 = c.add_page("B", Color::GREEN);
let idx2 = c.add_page("C", Color::BLUE);
assert_eq!(idx0, 0);
assert_eq!(idx1, 1);
assert_eq!(idx2, 2);
assert_eq!(c.page_count(), 3);
}
#[test]
fn carousel_empty_draw_does_not_panic() {
let mut c = Carousel::new(Rect::new(0, 0, 100, 50));
let svg = crate::widget::svg::render_to_svg(&mut c);
assert!(svg.starts_with("<svg"));
}
#[test]
fn carousel_svg_output() {
let mut c = default_carousel();
let svg = crate::widget::svg::render_to_svg(&mut c);
assert!(svg.starts_with("<svg"));
assert!(svg.contains("Page 1") || svg.contains("rect") || svg.contains("fill="));
}
#[test]
fn carousel_previous_at_zero_does_nothing() {
let mut c = default_carousel();
assert_eq!(c.current(), 0);
c.previous();
assert_eq!(c.current(), 0);
}
#[test]
fn carousel_next_at_last_does_nothing() {
let mut c = default_carousel();
c.set_current(2);
assert_eq!(c.current(), 2);
c.next();
assert_eq!(c.current(), 2);
}
#[test]
fn carousel_pages_returns_all() {
let c = default_carousel();
let pages = c.pages();
assert_eq!(pages.len(), 3);
assert_eq!(pages[0].title, "Page 1");
assert_eq!(pages[1].title, "Page 2");
assert_eq!(pages[2].title, "Page 3");
}
#[test]
fn carousel_page_content_is_stored_and_laid_out() {
let mut c = Carousel::new(Rect::new(0, 0, 300, 200));
c.add_page("Host", Color::WHITE);
let child = Label::new("Inside".to_string(), Rect::new(0, 0, 10, 10));
assert!(c.set_page_content(0, Box::new(child)));
assert!(c.pages()[0].has_content());
let _ = render_rgba(&mut c, Size::new(300, 200));
let laid_out = c.pages()[0].content().unwrap().geometry();
assert_eq!(laid_out.width, c.content_rect().width);
assert_eq!(laid_out.height, c.content_rect().height);
assert_eq!(laid_out.x, c.content_rect().x);
}
#[test]
fn carousel_set_page_content_rejects_unknown_index() {
let mut c = Carousel::new(Rect::new(0, 0, 300, 200));
c.add_page("Only", Color::WHITE);
let child = Label::new("Lost".to_string(), Rect::new(0, 0, 10, 10));
assert!(!c.set_page_content(7, Box::new(child)));
}
#[test]
fn carousel_remove_page_clamps_current() {
let mut c = default_carousel();
c.set_current(2);
let removed = c.remove_page(2);
assert!(removed.is_some());
assert_eq!(c.page_count(), 2);
assert_eq!(c.current(), 1, "removing the visible page shows the neighbour");
assert!(c.remove_page(9).is_none());
}
#[test]
fn carousel_swipe_past_half_width_lands_on_adjacent_page() {
let mut c = default_carousel();
assert_eq!(c.current(), 0);
c.handle_event(&Event::MousePress { pos: Point::new(280, 100), button: 1 });
c.handle_event(&Event::MouseMove { pos: Point::new(100, 100) });
c.handle_event(&Event::MouseRelease { pos: Point::new(100, 100), button: 1 });
assert_eq!(c.current(), 1, "a leftward swipe of 180px must advance one page");
}
#[test]
fn carousel_swipe_backwards_lands_on_previous_page() {
let mut c = default_carousel();
c.set_current(2);
c.handle_event(&Event::MousePress { pos: Point::new(20, 100), button: 1 });
c.handle_event(&Event::MouseMove { pos: Point::new(200, 100) });
c.handle_event(&Event::MouseRelease { pos: Point::new(200, 100), button: 1 });
assert_eq!(c.current(), 1, "a rightward swipe must go back one page");
}
#[test]
fn carousel_drag_below_threshold_does_not_page_by_swipe() {
let mut c = default_carousel();
c.handle_event(&Event::MousePress { pos: Point::new(100, 100), button: 1 });
c.handle_event(&Event::MouseMove { pos: Point::new(80, 100) });
c.handle_event(&Event::MouseRelease { pos: Point::new(80, 100), button: 1 });
assert_eq!(c.current(), 0);
}
#[test]
fn carousel_swipe_without_loop_stops_at_edge() {
let mut c = default_carousel();
assert!(!c.r#loop());
assert_eq!(c.current(), 0);
c.handle_event(&Event::MousePress { pos: Point::new(20, 100), button: 1 });
c.handle_event(&Event::MouseMove { pos: Point::new(250, 100) });
c.handle_event(&Event::MouseRelease { pos: Point::new(250, 100), button: 1 });
assert_eq!(c.current(), 0);
}
#[test]
fn carousel_swipe_with_loop_wraps_at_edge() {
let mut c = default_carousel();
c.set_loop(true);
c.set_current(2);
c.handle_event(&Event::MousePress { pos: Point::new(280, 100), button: 1 });
c.handle_event(&Event::MouseMove { pos: Point::new(100, 100) });
c.handle_event(&Event::MouseRelease { pos: Point::new(100, 100), button: 1 });
assert_eq!(c.current(), 0);
}
#[test]
fn carousel_mousedown_then_mouseup_is_a_click_not_a_drag() {
let mut c = default_carousel();
c.handle_event(&Event::MousePress { pos: Point::new(250, 100), button: 1 });
c.handle_event(&Event::MouseRelease { pos: Point::new(250, 100), button: 1 });
assert_eq!(c.current(), 1, "a press and release with no travel is a click");
}
#[test]
fn carousel_distance_alone_still_pages_regardless_of_speed() {
let c = default_carousel();
assert_eq!(c.swipe_direction_at(-60, 0.0), Some(1));
assert_eq!(c.swipe_direction_at(60, 0.0), Some(-1));
assert_eq!(c.swipe_direction_at(10, 0.0), None);
}
#[test]
fn carousel_a_fast_flick_pages_below_the_distance_threshold() {
let c = default_carousel();
assert_eq!(c.swipe_direction_at(-20, -800.0), Some(1));
assert_eq!(c.swipe_direction_at(20, 800.0), Some(-1));
assert_eq!(c.swipe_direction_at(-20, -100.0), None);
}
#[test]
fn carousel_a_flick_below_the_anti_jitter_floor_never_pages() {
let c = default_carousel();
assert_eq!(c.swipe_direction_at(-1, -100_000.0), None);
assert_eq!(c.swipe_direction_at(1, 100_000.0), None);
assert_eq!(c.min_flick_px(), 6);
assert_eq!(c.swipe_direction_at(-6, -500.0), Some(1));
}
#[test]
fn carousel_velocity_is_zero_without_two_timed_samples() {
let now = Instant::now();
assert_eq!(swipe_velocity_px_per_sec(100, 80, None, now, 300.0), 0.0);
assert_eq!(swipe_velocity_px_per_sec(100, 80, Some(now), now, 300.0), 0.0);
}
#[test]
fn carousel_velocity_is_measured_from_the_last_sample() {
let start = Instant::now();
let quarter_second_later = start + core::time::Duration::from_millis(250);
let rightward =
swipe_velocity_px_per_sec(100, 200, Some(start), quarter_second_later, 300.0);
assert!(
(rightward - 400.0).abs() < 1.0,
"100px in 250ms must read as 400px/s, got {rightward}"
);
let leftward =
swipe_velocity_px_per_sec(200, 100, Some(start), quarter_second_later, 300.0);
assert!((leftward + 400.0).abs() < 1.0, "leftward must be negative: {leftward}");
assert_eq!(
swipe_velocity_px_per_sec(100, 200, Some(quarter_second_later), start, 300.0),
0.0
);
}
#[test]
fn carousel_velocity_is_capped_well_above_the_flick_threshold() {
let start = Instant::now();
let instant = start + core::time::Duration::from_nanos(1);
let speed = swipe_velocity_px_per_sec(0, 10_000, Some(start), instant, 300.0);
assert!(speed <= 3000.0, "speed must be capped at ten widths per second: {speed}");
let real_flick = swipe_velocity_px_per_sec(
0,
100,
Some(start),
start + core::time::Duration::from_millis(100),
300.0,
);
assert!(
real_flick >= FLICK_VELOCITY_PX_PER_SEC,
"{real_flick} must exceed the flick \
threshold and must not be capped below it"
);
}
#[test]
fn carousel_short_fast_drag_pages_end_to_end() {
let mut c = default_carousel();
assert_eq!(c.current(), 0);
c.handle_event(&Event::MousePress { pos: Point::new(280, 100), button: 1 });
c.handle_event(&Event::MouseMove { pos: Point::new(280 - 60, 100) });
c.handle_event(&Event::MouseMove { pos: Point::new(280 - 66, 100) });
c.handle_event(&Event::MouseRelease { pos: Point::new(280 - 66, 100), button: 1 });
assert_eq!(c.current(), 1, "a 66px leftward drag must page forward");
}
#[test]
fn carousel_swipe_entering_state_survives_a_fast_release() {
let mut c = default_carousel();
c.handle_event(&Event::MousePress { pos: Point::new(280, 100), button: 1 });
c.handle_event(&Event::MouseMove { pos: Point::new(220, 100) });
c.handle_event(&Event::MouseRelease { pos: Point::new(220, 100), button: 1 });
assert_eq!(c.current(), 1);
c.handle_event(&Event::MousePress { pos: Point::new(20, 100), button: 1 });
c.handle_event(&Event::MouseMove { pos: Point::new(80, 100) });
c.handle_event(&Event::MouseRelease { pos: Point::new(80, 100), button: 1 });
assert_eq!(c.current(), 0);
}
#[test]
fn carousel_loop_wraps_both_directions() {
let mut c = default_carousel();
c.set_loop(true);
c.previous();
assert_eq!(c.current(), 2, "previous from the first page wraps to the last");
c.next();
assert_eq!(c.current(), 0, "next from the last page wraps to the first");
}
#[test]
fn carousel_autoplay_advances_by_elapsed_time() {
let mut c = default_carousel();
c.set_autoplay(Some(core::time::Duration::from_millis(1000)));
c.advance_autoplay_by(core::time::Duration::from_millis(400));
assert_eq!(c.current(), 0);
c.advance_autoplay_by(core::time::Duration::from_millis(700));
assert_eq!(c.current(), 1);
}
#[test]
fn carousel_autoplay_without_loop_stops_at_last_page() {
let mut c = default_carousel();
c.set_autoplay(Some(core::time::Duration::from_millis(100)));
assert!(!c.r#loop());
for _ in 0..10 {
c.advance_autoplay_by(core::time::Duration::from_millis(100));
}
assert_eq!(c.current(), 2, "autoplay must stop at the last page when loop is off");
}
#[test]
fn carousel_autoplay_hover_pauses() {
let mut c = default_carousel();
c.set_autoplay(Some(core::time::Duration::from_millis(100)));
c.handle_event(&Event::MouseEnter { pos: Point::new(10, 10) });
c.advance_autoplay_by(core::time::Duration::from_millis(500));
assert_eq!(c.current(), 0, "hover must hold autoplay");
c.handle_event(&Event::MouseLeave { pos: Point::new(10, 10) });
c.advance_autoplay_by(core::time::Duration::from_millis(500));
assert_ne!(c.current(), 0, "autoplay resumes once the pointer leaves");
}
#[test]
fn carousel_autoplay_paused_while_disabled() {
let mut c = default_carousel();
c.set_autoplay(Some(core::time::Duration::from_millis(100)));
c.set_enabled(false);
c.advance_autoplay_by(core::time::Duration::from_millis(1000));
assert_eq!(c.current(), 0);
}
#[test]
fn carousel_autoplay_zero_interval_is_rejected() {
let mut c = default_carousel();
c.set_autoplay(Some(core::time::Duration::from_millis(0)));
assert!(c.autoplay().is_none());
c.advance_autoplay_by(core::time::Duration::from_millis(1000));
assert_eq!(c.current(), 0);
}
#[test]
fn carousel_autoplay_restarts_dwell_on_manual_move() {
let mut c = default_carousel();
c.set_autoplay(Some(core::time::Duration::from_millis(1000)));
c.advance_autoplay_by(core::time::Duration::from_millis(900));
c.set_current(2);
assert_eq!(c.current(), 2);
c.advance_autoplay_by(core::time::Duration::from_millis(200));
assert_eq!(c.current(), 2);
}
#[test]
fn carousel_hidden_indicator_draws_nothing() {
let mut c = default_carousel();
let size = Size::new(300, 200);
let with_dots = render_rgba(&mut c, size);
let bottom_y = size.height - 8;
let dots_row: Vec<_> =
(0..size.width).map(|x| pixel(&with_dots, size.width, x, bottom_y)).collect();
let page_color = (52u8, 120u8, 246u8);
assert!(
dots_row.iter().any(|p| (p.0, p.1, p.2) != page_color),
"the dot indicator must paint something over the page background"
);
c.set_indicator_style(CarouselIndicatorStyle::Hidden);
let without_dots = render_rgba(&mut c, size);
let hidden_row: Vec<_> =
(0..size.width).map(|x| pixel(&without_dots, size.width, x, bottom_y)).collect();
let painted =
hidden_row.iter().filter(|p| (p.0, p.1, p.2) != page_color && p.3 != 0).count();
assert!(painted < 20, "a hidden indicator must not paint the dot row: {painted} pixels");
}
#[test]
fn carousel_indicator_style_round_trips() {
let mut c = default_carousel();
for style in [
CarouselIndicatorStyle::Dots,
CarouselIndicatorStyle::Bars,
CarouselIndicatorStyle::Numeric,
CarouselIndicatorStyle::Hidden,
] {
c.set_indicator_style(style);
assert_eq!(c.indicator_style(), style);
}
assert!(!CarouselIndicatorStyle::Hidden.is_visible());
assert!(CarouselIndicatorStyle::Bars.is_visible());
assert!(CarouselIndicatorStyle::Numeric.is_visible());
assert!(CarouselIndicatorStyle::Dots.is_per_page());
assert!(CarouselIndicatorStyle::Bars.is_per_page());
assert!(!CarouselIndicatorStyle::Numeric.is_per_page());
assert!(!CarouselIndicatorStyle::Hidden.is_per_page());
}
#[test]
fn carousel_numeric_indicator_paints_the_page_counter() {
let mut c = default_carousel();
c.set_indicator_style(CarouselIndicatorStyle::Numeric);
let size = Size::new(300, 200);
let page_color = (52u8, 120u8, 246u8);
let strip_y = size.height - 8;
let at_page_0 = render_rgba(&mut c, size);
let painted = |rgba: &[u8]| {
(0..size.width)
.map(|x| pixel(rgba, size.width, x, strip_y))
.filter(|p| (p.0, p.1, p.2) != page_color && p.3 != 0)
.count()
};
let first = painted(&at_page_0);
assert!(first > 0, "a numeric indicator must paint the counter text");
c.set_current(2);
let at_page_2 = render_rgba(&mut c, size);
assert_ne!(
at_page_0, at_page_2,
"paging must change the counter, so the two frames cannot be identical"
);
}
#[test]
fn carousel_numeric_indicator_does_not_scale_with_page_count() {
let size = Size::new(300, 200);
let strip_y = size.height - 8;
let page_color = (52u8, 120u8, 246u8);
let painted = |rgba: &[u8]| {
(0..size.width)
.map(|x| pixel(rgba, size.width, x, strip_y))
.filter(|p| (p.0, p.1, p.2) != page_color && p.3 != 0)
.count()
};
let mut few = Carousel::new(Rect::new(0, 0, 300, 200));
few.add_page("A", Color::rgb(52, 120, 246));
few.add_page("B", Color::rgb(52, 120, 246));
few.set_indicator_style(CarouselIndicatorStyle::Numeric);
let mut many = Carousel::new(Rect::new(0, 0, 300, 200));
for i in 0..12 {
many.add_page(format!("P{i}"), Color::rgb(52, 120, 246));
}
many.set_indicator_style(CarouselIndicatorStyle::Numeric);
let few_ink = painted(&render_rgba(&mut few, size));
let many_ink = painted(&render_rgba(&mut many, size));
assert!(few_ink > 0 && many_ink > 0);
let ratio = many_ink as f32 / few_ink as f32;
assert!(ratio < 2.0, "the counter must not scale with page count: {ratio}");
}
#[test]
fn carousel_indicator_position_changes_content_rect() {
let mut c = default_carousel();
c.set_indicator_position(CarouselIndicatorPosition::Bottom);
let bottom = c.content_rect();
assert_eq!(bottom.y, 0);
assert_eq!(bottom.height, 200 - INDICATOR_STRIP);
c.set_indicator_position(CarouselIndicatorPosition::Top);
let top = c.content_rect();
assert_eq!(top.y, INDICATOR_STRIP as i32);
assert_eq!(top.height, 200 - INDICATOR_STRIP);
c.set_indicator_position(CarouselIndicatorPosition::Left);
let left = c.content_rect();
assert_eq!(left.x, INDICATOR_STRIP as i32);
assert_eq!(left.width, 300 - INDICATOR_STRIP);
c.set_indicator_position(CarouselIndicatorPosition::Right);
let right = c.content_rect();
assert_eq!(right.x, 0);
assert_eq!(right.width, 300 - INDICATOR_STRIP);
}
#[test]
fn carousel_single_page_reserves_no_indicator_strip() {
let mut c = Carousel::new(Rect::new(0, 0, 300, 200));
c.add_page("Only", Color::WHITE);
assert_eq!(c.content_rect().height, 200);
}
#[test]
fn carousel_loop_property_round_trips() {
let mut c = default_carousel();
assert_eq!(c.get("loop").unwrap(), CapabilityValue::Bool(false));
c.set("loop", CapabilityValue::Bool(true)).unwrap();
assert!(c.r#loop());
assert_eq!(c.get("loop").unwrap(), CapabilityValue::Bool(true));
}
#[test]
fn carousel_autoplay_interval_property_round_trips() {
let mut c = default_carousel();
assert_eq!(c.get("autoplay_interval").unwrap(), CapabilityValue::Null);
c.set("autoplay_interval", CapabilityValue::UInt(1500)).unwrap();
assert_eq!(c.get("autoplay_interval").unwrap(), CapabilityValue::UInt(1500));
assert_eq!(c.autoplay(), Some(core::time::Duration::from_millis(1500)));
c.set("autoplay_interval", CapabilityValue::Null).unwrap();
assert_eq!(c.get("autoplay_interval").unwrap(), CapabilityValue::Null);
assert!(c.autoplay().is_none());
}
#[test]
fn carousel_indicator_properties_round_trip() {
let mut c = default_carousel();
for name in ["dots", "bars", "numeric", "hidden"] {
c.set("indicator_style", CapabilityValue::String(name.to_string())).unwrap();
assert_eq!(
c.get("indicator_style").unwrap(),
CapabilityValue::String(name.to_string())
);
}
for name in ["bottom", "top", "left", "right"] {
c.set("indicator_position", CapabilityValue::String(name.to_string())).unwrap();
assert_eq!(
c.get("indicator_position").unwrap(),
CapabilityValue::String(name.to_string())
);
}
assert!(c.set("indicator_style", CapabilityValue::String("sparkle".to_string())).is_err());
}
#[test]
fn carousel_derived_properties_are_read_only() {
let mut c = default_carousel();
assert!(c.set("item_count", CapabilityValue::UInt(9)).is_err());
assert!(c.set("current_page_title", CapabilityValue::String("x".into())).is_err());
assert_eq!(c.get("item_count").unwrap(), CapabilityValue::UInt(3));
assert_eq!(
c.get("current_page_title").unwrap(),
CapabilityValue::String("Page 1".to_string())
);
}
#[test]
fn carousel_forwards_arrow_keys_to_the_visible_page() {
let mut c = Carousel::new(Rect::new(0, 0, 300, 200));
c.add_page("Host", Color::WHITE);
c.add_page("Other", Color::WHITE);
let child = Label::new("Hello".to_string(), Rect::new(0, 0, 300, 200));
assert!(c.set_page_content(0, Box::new(child)));
c.handle_event(&Event::KeyPress { key: 39, modifiers: 0 });
assert_eq!(c.current(), 1);
c.handle_event(&Event::KeyPress { key: 37, modifiers: 0 });
assert_eq!(c.current(), 0);
assert!(c.pages()[0].has_content());
}
#[test]
fn carousel_content_draw_does_not_panic_and_paints() {
let mut c = Carousel::new(Rect::new(0, 0, 300, 200));
c.add_page("Host", Color::WHITE);
let child = Label::new("Inside".to_string(), Rect::new(0, 0, 300, 200));
assert!(c.set_page_content(0, Box::new(child)));
let rgba = render_rgba(&mut c, Size::new(300, 200));
assert!(!rgba.is_empty());
let mut non_white = 0;
for chunk in rgba.chunks_exact(4) {
if chunk[0] != 255 || chunk[1] != 255 || chunk[2] != 255 {
non_white += 1;
}
}
assert!(non_white > 0, "a page with content must paint the content");
}
}