use presentar_core::{
widget::LayoutResult, Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas,
Constraints, Event, Key, Rect, Size, TypeId, Widget,
};
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::ops::Range;
use std::time::Duration;
pub type RenderItemFn = Box<dyn Fn(usize, &ListItem) -> Box<dyn Widget> + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ListDirection {
#[default]
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SelectionMode {
#[default]
None,
Single,
Multiple,
}
#[derive(Debug, Clone)]
pub struct ListItem {
pub key: String,
pub size: f32,
pub selected: bool,
}
impl ListItem {
#[must_use]
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
size: 48.0, selected: false,
}
}
#[must_use]
pub const fn size(mut self, size: f32) -> Self {
self.size = size;
self
}
#[must_use]
pub const fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
#[derive(Serialize, Deserialize)]
pub struct List {
pub direction: ListDirection,
pub selection_mode: SelectionMode,
pub item_height: Option<f32>,
pub gap: f32,
pub scroll_offset: f32,
#[serde(skip)]
items: Vec<ListItem>,
#[serde(skip)]
selected: Vec<usize>,
#[serde(skip)]
focused_index: Option<usize>,
#[serde(skip)]
bounds: Rect,
#[serde(skip)]
visible_range: Range<usize>,
#[serde(skip)]
item_positions: Vec<f32>,
#[serde(skip)]
content_size: f32,
test_id_value: Option<String>,
#[serde(skip)]
children: Vec<Box<dyn Widget>>,
#[serde(skip)]
render_item: Option<RenderItemFn>,
}
impl Default for List {
fn default() -> Self {
Self {
direction: ListDirection::Vertical,
selection_mode: SelectionMode::None,
item_height: Some(48.0),
gap: 0.0,
scroll_offset: 0.0,
items: Vec::new(),
selected: Vec::new(),
focused_index: None,
bounds: Rect::default(),
visible_range: 0..0,
item_positions: Vec::new(),
content_size: 0.0,
test_id_value: None,
children: Vec::new(),
render_item: None,
}
}
}
impl List {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn direction(mut self, direction: ListDirection) -> Self {
self.direction = direction;
self
}
#[must_use]
pub const fn selection_mode(mut self, mode: SelectionMode) -> Self {
self.selection_mode = mode;
self
}
#[must_use]
pub const fn item_height(mut self, height: f32) -> Self {
self.item_height = Some(height);
self
}
#[must_use]
pub const fn gap(mut self, gap: f32) -> Self {
self.gap = gap;
self
}
pub fn items(mut self, items: impl IntoIterator<Item = ListItem>) -> Self {
self.items = items.into_iter().collect();
self.recalculate_positions();
self
}
pub fn render_with<F>(mut self, f: F) -> Self
where
F: Fn(usize, &ListItem) -> Box<dyn Widget> + Send + Sync + 'static,
{
contract_pre_render!();
self.render_item = Some(Box::new(f));
self
}
#[must_use]
pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
self.test_id_value = Some(id.into());
self
}
#[must_use]
pub fn item_count(&self) -> usize {
self.items.len()
}
#[must_use]
pub fn selected_indices(&self) -> &[usize] {
&self.selected
}
#[must_use]
pub fn visible_range(&self) -> Range<usize> {
self.visible_range.clone()
}
#[must_use]
pub const fn content_size(&self) -> f32 {
self.content_size
}
pub fn scroll_to(&mut self, index: usize) {
if index >= self.items.len() {
return;
}
let item_pos = self.item_positions.get(index).copied().unwrap_or(0.0);
let viewport_size = match self.direction {
ListDirection::Vertical => self.bounds.height,
ListDirection::Horizontal => self.bounds.width,
};
self.scroll_offset = item_pos.min(self.content_size - viewport_size).max(0.0);
}
pub fn scroll_into_view(&mut self, index: usize) {
if index >= self.items.len() {
return;
}
let item_pos = self.item_positions.get(index).copied().unwrap_or(0.0);
let item_size = self.get_item_size(index);
let viewport_size = match self.direction {
ListDirection::Vertical => self.bounds.height,
ListDirection::Horizontal => self.bounds.width,
};
let item_end = item_pos + item_size;
let viewport_end = self.scroll_offset + viewport_size;
if item_pos < self.scroll_offset {
self.scroll_offset = item_pos;
} else if item_end > viewport_end {
self.scroll_offset = (item_end - viewport_size).max(0.0);
}
}
pub fn select(&mut self, index: usize) {
match self.selection_mode {
SelectionMode::None => {}
SelectionMode::Single => {
self.selected.clear();
if index < self.items.len() {
self.selected.push(index);
self.items[index].selected = true;
}
}
SelectionMode::Multiple => {
if index < self.items.len() && !self.selected.contains(&index) {
self.selected.push(index);
self.items[index].selected = true;
}
}
}
}
pub fn deselect(&mut self, index: usize) {
if let Some(pos) = self.selected.iter().position(|&i| i == index) {
self.selected.remove(pos);
if index < self.items.len() {
self.items[index].selected = false;
}
}
}
pub fn toggle_selection(&mut self, index: usize) {
if self.selected.contains(&index) {
self.deselect(index);
} else {
self.select(index);
}
}
pub fn clear_selection(&mut self) {
for &i in &self.selected {
if i < self.items.len() {
self.items[i].selected = false;
}
}
self.selected.clear();
}
fn get_item_size(&self, index: usize) -> f32 {
if let Some(fixed) = self.item_height {
fixed
} else {
self.items.get(index).map_or(48.0, |i| i.size)
}
}
fn recalculate_positions(&mut self) {
self.item_positions.clear();
let mut pos = 0.0;
for (i, item) in self.items.iter().enumerate() {
self.item_positions.push(pos);
let size = self.item_height.unwrap_or(item.size);
pos += size;
if i < self.items.len() - 1 {
pos += self.gap;
}
}
self.content_size = pos;
}
fn calculate_visible_range(&mut self, viewport_size: f32) {
if self.items.is_empty() {
self.visible_range = 0..0;
return;
}
let start_offset = self.scroll_offset;
let end_offset = self.scroll_offset + viewport_size;
let first = self
.item_positions
.partition_point(|&pos| pos + self.get_item_size(0) < start_offset);
let mut last = first;
for i in first..self.items.len() {
let pos = self.item_positions.get(i).copied().unwrap_or(0.0);
if pos > end_offset {
break;
}
last = i + 1;
}
let buffer = 2;
let start = first.saturating_sub(buffer);
let end = (last + buffer).min(self.items.len());
self.visible_range = start..end;
}
fn render_visible_items(&mut self) {
self.children.clear();
if self.render_item.is_none() {
return;
}
for i in self.visible_range.clone() {
if let Some(item) = self.items.get(i) {
if let Some(ref render) = self.render_item {
let widget = render(i, item);
self.children.push(widget);
}
}
}
}
}
impl Widget for List {
fn type_id(&self) -> TypeId {
TypeId::of::<Self>()
}
fn measure(&self, constraints: Constraints) -> Size {
constraints.constrain(Size::new(constraints.max_width, constraints.max_height))
}
fn layout(&mut self, bounds: Rect) -> LayoutResult {
self.bounds = bounds;
let viewport_size = match self.direction {
ListDirection::Vertical => bounds.height,
ListDirection::Horizontal => bounds.width,
};
self.calculate_visible_range(viewport_size);
self.render_visible_items();
for (local_idx, i) in self.visible_range.clone().enumerate() {
if local_idx >= self.children.len() {
break;
}
let item_pos = self.item_positions.get(i).copied().unwrap_or(0.0);
let item_size = self.get_item_size(i);
let item_bounds = match self.direction {
ListDirection::Vertical => Rect::new(
bounds.x,
bounds.y + item_pos - self.scroll_offset,
bounds.width,
item_size,
),
ListDirection::Horizontal => Rect::new(
bounds.x + item_pos - self.scroll_offset,
bounds.y,
item_size,
bounds.height,
),
};
self.children[local_idx].layout(item_bounds);
}
LayoutResult {
size: bounds.size(),
}
}
fn paint(&self, canvas: &mut dyn Canvas) {
canvas.push_clip(self.bounds);
for child in &self.children {
child.paint(canvas);
}
canvas.pop_clip();
}
fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
match event {
Event::Scroll { delta_y, .. } => {
let viewport_size = match self.direction {
ListDirection::Vertical => self.bounds.height,
ListDirection::Horizontal => self.bounds.width,
};
let max_scroll = (self.content_size - viewport_size).max(0.0);
self.scroll_offset = (self.scroll_offset - delta_y * 48.0).clamp(0.0, max_scroll);
self.calculate_visible_range(viewport_size);
self.render_visible_items();
Some(Box::new(ListScrolled {
offset: self.scroll_offset,
}))
}
Event::KeyDown { key, .. } => {
if let Some(focused) = self.focused_index {
match key {
Key::Up | Key::Left => {
if focused > 0 {
self.focused_index = Some(focused - 1);
self.scroll_into_view(focused - 1);
}
}
Key::Down | Key::Right => {
if focused < self.items.len() - 1 {
self.focused_index = Some(focused + 1);
self.scroll_into_view(focused + 1);
}
}
Key::Enter | Key::Space => {
self.toggle_selection(focused);
return Some(Box::new(ListItemSelected { index: focused }));
}
Key::Home => {
self.focused_index = Some(0);
self.scroll_to(0);
}
Key::End => {
let last = self.items.len().saturating_sub(1);
self.focused_index = Some(last);
self.scroll_to(last);
}
_ => {}
}
}
None
}
Event::MouseDown { position, .. } => {
let pos = match self.direction {
ListDirection::Vertical => position.y - self.bounds.y + self.scroll_offset,
ListDirection::Horizontal => position.x - self.bounds.x + self.scroll_offset,
};
for (i, &item_pos) in self.item_positions.iter().enumerate() {
let item_size = self.get_item_size(i);
if pos >= item_pos && pos < item_pos + item_size {
self.focused_index = Some(i);
self.toggle_selection(i);
return Some(Box::new(ListItemClicked { index: i }));
}
}
None
}
_ => {
for child in &mut self.children {
if let Some(msg) = child.event(event) {
return Some(msg);
}
}
None
}
}
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
&mut self.children
}
fn is_focusable(&self) -> bool {
true
}
fn test_id(&self) -> Option<&str> {
self.test_id_value.as_deref()
}
fn bounds(&self) -> Rect {
self.bounds
}
}
impl Brick for List {
fn brick_name(&self) -> &'static str {
"List"
}
fn assertions(&self) -> &[BrickAssertion] {
&[BrickAssertion::MaxLatencyMs(16)]
}
fn budget(&self) -> BrickBudget {
BrickBudget::uniform(16)
}
fn verify(&self) -> BrickVerification {
BrickVerification {
passed: self.assertions().to_vec(),
failed: vec![],
verification_time: Duration::from_micros(10),
}
}
fn to_html(&self) -> String {
r#"<div class="brick-list"></div>"#.to_string()
}
fn to_css(&self) -> String {
".brick-list { display: block; overflow: auto; }".to_string()
}
fn test_id(&self) -> Option<&str> {
self.test_id_value.as_deref()
}
}
#[derive(Debug, Clone)]
pub struct ListScrolled {
pub offset: f32,
}
#[derive(Debug, Clone)]
pub struct ListItemClicked {
pub index: usize,
}
#[derive(Debug, Clone)]
pub struct ListItemSelected {
pub index: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_direction_default() {
assert_eq!(ListDirection::default(), ListDirection::Vertical);
}
#[test]
fn test_selection_mode_default() {
assert_eq!(SelectionMode::default(), SelectionMode::None);
}
#[test]
fn test_list_item_new() {
let item = ListItem::new("item-1");
assert_eq!(item.key, "item-1");
assert_eq!(item.size, 48.0);
assert!(!item.selected);
}
#[test]
fn test_list_item_builder() {
let item = ListItem::new("item-1").size(64.0).selected(true);
assert_eq!(item.size, 64.0);
assert!(item.selected);
}
#[test]
fn test_list_new() {
let list = List::new();
assert_eq!(list.direction, ListDirection::Vertical);
assert_eq!(list.selection_mode, SelectionMode::None);
assert_eq!(list.item_height, Some(48.0));
assert_eq!(list.gap, 0.0);
assert_eq!(list.item_count(), 0);
}
#[test]
fn test_list_builder() {
let list = List::new()
.direction(ListDirection::Horizontal)
.selection_mode(SelectionMode::Single)
.item_height(32.0)
.gap(8.0);
assert_eq!(list.direction, ListDirection::Horizontal);
assert_eq!(list.selection_mode, SelectionMode::Single);
assert_eq!(list.item_height, Some(32.0));
assert_eq!(list.gap, 8.0);
}
#[test]
fn test_list_items() {
let items = vec![ListItem::new("1"), ListItem::new("2"), ListItem::new("3")];
let list = List::new().items(items);
assert_eq!(list.item_count(), 3);
}
#[test]
fn test_list_content_size() {
let items = vec![ListItem::new("1"), ListItem::new("2"), ListItem::new("3")];
let list = List::new().item_height(50.0).gap(10.0).items(items);
assert_eq!(list.content_size(), 170.0);
}
#[test]
fn test_list_content_size_variable_height() {
let items = vec![
ListItem::new("1").size(30.0),
ListItem::new("2").size(40.0),
ListItem::new("3").size(50.0),
];
let mut list = List::new().gap(5.0);
list.item_height = None; list = list.items(items);
assert_eq!(list.content_size(), 130.0);
}
#[test]
fn test_list_select_single() {
let items = vec![ListItem::new("1"), ListItem::new("2")];
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.items(items);
list.select(0);
assert_eq!(list.selected_indices(), &[0]);
list.select(1);
assert_eq!(list.selected_indices(), &[1]); }
#[test]
fn test_list_select_multiple() {
let items = vec![ListItem::new("1"), ListItem::new("2")];
let mut list = List::new()
.selection_mode(SelectionMode::Multiple)
.items(items);
list.select(0);
list.select(1);
assert_eq!(list.selected_indices(), &[0, 1]);
}
#[test]
fn test_list_deselect() {
let items = vec![ListItem::new("1"), ListItem::new("2")];
let mut list = List::new()
.selection_mode(SelectionMode::Multiple)
.items(items);
list.select(0);
list.select(1);
list.deselect(0);
assert_eq!(list.selected_indices(), &[1]);
}
#[test]
fn test_list_toggle_selection() {
let items = vec![ListItem::new("1")];
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.items(items);
list.toggle_selection(0);
assert_eq!(list.selected_indices(), &[0]);
list.toggle_selection(0);
assert!(list.selected_indices().is_empty());
}
#[test]
fn test_list_clear_selection() {
let items = vec![ListItem::new("1"), ListItem::new("2")];
let mut list = List::new()
.selection_mode(SelectionMode::Multiple)
.items(items);
list.select(0);
list.select(1);
list.clear_selection();
assert!(list.selected_indices().is_empty());
}
#[test]
fn test_list_scroll_to() {
let items: Vec<_> = (0..100).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().item_height(50.0).items(items);
list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
list.scroll_to(10);
assert_eq!(list.scroll_offset, 500.0); }
#[test]
fn test_list_scroll_into_view() {
let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().item_height(50.0).items(items);
list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
list.scroll_offset = 0.0;
list.scroll_into_view(5);
assert_eq!(list.scroll_offset, 100.0);
}
#[test]
fn test_list_visible_range() {
let items: Vec<_> = (0..100).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().item_height(50.0).items(items);
list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
list.scroll_offset = 0.0;
list.calculate_visible_range(200.0);
let range = list.visible_range();
assert!(range.start <= 4);
assert!(range.end >= 4);
}
#[test]
fn test_list_measure() {
let list = List::new();
let size = list.measure(Constraints::loose(Size::new(300.0, 400.0)));
assert_eq!(size, Size::new(300.0, 400.0));
}
#[test]
fn test_list_layout() {
let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().item_height(50.0).items(items);
let result = list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
assert_eq!(result.size, Size::new(300.0, 200.0));
assert_eq!(list.bounds, Rect::new(0.0, 0.0, 300.0, 200.0));
}
#[test]
fn test_list_type_id() {
let list = List::new();
assert_eq!(Widget::type_id(&list), TypeId::of::<List>());
}
#[test]
fn test_list_is_focusable() {
let list = List::new();
assert!(list.is_focusable());
}
#[test]
fn test_list_test_id() {
let list = List::new().with_test_id("my-list");
assert_eq!(Widget::test_id(&list), Some("my-list"));
}
#[test]
fn test_list_children_empty() {
let list = List::new();
assert!(list.children().is_empty());
}
#[test]
fn test_list_bounds() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().items(items);
list.layout(Rect::new(10.0, 20.0, 300.0, 200.0));
assert_eq!(list.bounds(), Rect::new(10.0, 20.0, 300.0, 200.0));
}
#[test]
fn test_list_scrolled_message() {
let msg = ListScrolled { offset: 100.0 };
assert_eq!(msg.offset, 100.0);
}
#[test]
fn test_list_item_clicked_message() {
let msg = ListItemClicked { index: 5 };
assert_eq!(msg.index, 5);
}
#[test]
fn test_list_item_selected_message() {
let msg = ListItemSelected { index: 3 };
assert_eq!(msg.index, 3);
}
#[test]
fn test_list_direction_horizontal() {
let list = List::new().direction(ListDirection::Horizontal);
assert_eq!(list.direction, ListDirection::Horizontal);
}
#[test]
fn test_list_direction_is_vertical_by_default() {
assert_eq!(ListDirection::default(), ListDirection::Vertical);
}
#[test]
fn test_selection_mode_is_none_by_default() {
assert_eq!(SelectionMode::default(), SelectionMode::None);
}
#[test]
fn test_list_with_selection_mode_multiple() {
let list = List::new().selection_mode(SelectionMode::Multiple);
assert_eq!(list.selection_mode, SelectionMode::Multiple);
}
#[test]
fn test_list_with_selection_mode_single() {
let list = List::new().selection_mode(SelectionMode::Single);
assert_eq!(list.selection_mode, SelectionMode::Single);
}
#[test]
fn test_list_gap() {
let list = List::new().gap(10.0);
assert_eq!(list.gap, 10.0);
}
#[test]
fn test_list_item_height_custom() {
let list = List::new().item_height(60.0);
assert_eq!(list.item_height, Some(60.0));
}
#[test]
fn test_list_children_mut() {
let mut list = List::new();
assert!(list.children_mut().is_empty());
}
#[test]
fn test_list_content_size_calculated() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let list = List::new().items(items).item_height(40.0);
assert!(list.content_size() > 0.0);
}
#[test]
fn test_list_item_size_custom() {
let item = ListItem::new("Item").size(60.0);
assert_eq!(item.size, 60.0);
}
#[test]
fn test_list_item_selected_state() {
let item = ListItem::new("Item").selected(true);
assert!(item.selected);
}
#[test]
fn test_list_event_returns_none_when_empty() {
let mut list = List::new();
list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
let result = list.event(&Event::key_down(Key::Down));
assert!(result.is_none());
}
#[test]
fn test_list_scroll_event() {
let items: Vec<_> = (0..20).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().item_height(50.0).items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
let result = list.event(&Event::Scroll {
delta_x: 0.0,
delta_y: -2.0,
});
assert!(result.is_some());
assert!(list.scroll_offset > 0.0);
}
#[test]
fn test_list_scroll_event_clamp() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().item_height(50.0).items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 500.0));
let _ = list.event(&Event::Scroll {
delta_x: 0.0,
delta_y: -10.0,
});
assert_eq!(list.scroll_offset, 0.0);
}
#[test]
fn test_list_key_down_focused() {
let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
list.focused_index = Some(5);
let _ = list.event(&Event::key_down(Key::Down));
assert_eq!(list.focused_index, Some(6));
let _ = list.event(&Event::key_down(Key::Up));
assert_eq!(list.focused_index, Some(5));
}
#[test]
fn test_list_key_left_right() {
let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.direction(ListDirection::Horizontal)
.selection_mode(SelectionMode::Single)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
list.focused_index = Some(5);
let _ = list.event(&Event::key_down(Key::Right));
assert_eq!(list.focused_index, Some(6));
let _ = list.event(&Event::key_down(Key::Left));
assert_eq!(list.focused_index, Some(5));
}
#[test]
fn test_list_key_home_end() {
let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
list.focused_index = Some(5);
let _ = list.event(&Event::key_down(Key::Home));
assert_eq!(list.focused_index, Some(0));
let _ = list.event(&Event::key_down(Key::End));
assert_eq!(list.focused_index, Some(9));
}
#[test]
fn test_list_key_enter_selects() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
list.focused_index = Some(2);
let result = list.event(&Event::key_down(Key::Enter));
assert!(result.is_some());
assert_eq!(list.selected_indices(), &[2]);
}
#[test]
fn test_list_key_space_selects() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
list.focused_index = Some(3);
let result = list.event(&Event::key_down(Key::Space));
assert!(result.is_some());
assert_eq!(list.selected_indices(), &[3]);
}
#[test]
fn test_list_mouse_down_click() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 300.0));
let result = list.event(&Event::MouseDown {
position: presentar_core::Point::new(150.0, 75.0),
button: presentar_core::MouseButton::Left,
});
assert!(result.is_some());
assert_eq!(list.focused_index, Some(1));
}
#[test]
fn test_list_mouse_down_horizontal() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.direction(ListDirection::Horizontal)
.selection_mode(SelectionMode::Single)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 100.0));
let result = list.event(&Event::MouseDown {
position: presentar_core::Point::new(75.0, 50.0),
button: presentar_core::MouseButton::Left,
});
assert!(result.is_some());
assert_eq!(list.focused_index, Some(1));
}
#[test]
fn test_list_mouse_down_miss() {
let items: Vec<_> = (0..2).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 300.0));
let result = list.event(&Event::MouseDown {
position: presentar_core::Point::new(150.0, 200.0),
button: presentar_core::MouseButton::Left,
});
assert!(result.is_none());
}
#[test]
fn test_list_other_event() {
let mut list = List::new();
list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
let result = list.event(&Event::MouseMove {
position: presentar_core::Point::new(100.0, 100.0),
});
assert!(result.is_none());
}
use presentar_core::RecordingCanvas;
#[test]
fn test_list_paint_empty() {
let list = List::new();
let mut canvas = RecordingCanvas::new();
list.paint(&mut canvas);
}
#[test]
fn test_list_paint_with_items() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().item_height(50.0).items(items);
list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
let mut canvas = RecordingCanvas::new();
list.paint(&mut canvas);
}
#[test]
fn test_list_scroll_to_out_of_bounds() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().item_height(50.0).items(items);
list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
list.scroll_to(100);
assert_eq!(list.scroll_offset, 0.0);
}
#[test]
fn test_list_scroll_into_view_out_of_bounds() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().item_height(50.0).items(items);
list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
list.scroll_into_view(100);
assert_eq!(list.scroll_offset, 0.0);
}
#[test]
fn test_list_scroll_into_view_item_above() {
let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().item_height(50.0).items(items);
list.bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
list.scroll_offset = 200.0;
list.scroll_into_view(0);
assert_eq!(list.scroll_offset, 0.0);
}
#[test]
fn test_list_select_none_mode() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new().selection_mode(SelectionMode::None).items(items);
list.select(0);
assert!(list.selected_indices().is_empty());
}
#[test]
fn test_list_select_out_of_bounds() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.items(items);
list.select(100);
assert!(list.selected_indices().is_empty());
}
#[test]
fn test_list_select_multiple_same_item() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Multiple)
.items(items);
list.select(0);
list.select(0); assert_eq!(list.selected_indices().len(), 1);
}
#[test]
fn test_list_deselect_nonexistent() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Multiple)
.items(items);
list.select(0);
list.deselect(1); assert_eq!(list.selected_indices(), &[0]);
}
#[test]
fn test_list_clear_selection_with_invalid_indices() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Multiple)
.items(items);
list.select(0);
list.selected.push(100); list.clear_selection();
assert!(list.selected_indices().is_empty());
}
#[test]
fn test_list_horizontal_layout() {
let items: Vec<_> = (0..5).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.direction(ListDirection::Horizontal)
.item_height(50.0)
.items(items);
let result = list.layout(Rect::new(0.0, 0.0, 300.0, 100.0));
assert_eq!(result.size, Size::new(300.0, 100.0));
}
#[test]
fn test_list_horizontal_scroll() {
let items: Vec<_> = (0..20).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.direction(ListDirection::Horizontal)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
list.scroll_to(10);
assert_eq!(list.scroll_offset, 500.0);
}
#[test]
fn test_list_horizontal_scroll_into_view() {
let items: Vec<_> = (0..10).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.direction(ListDirection::Horizontal)
.item_height(50.0)
.items(items);
list.bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
list.scroll_offset = 0.0;
list.scroll_into_view(5);
assert!(list.scroll_offset > 0.0);
}
#[test]
fn test_list_visible_range_empty() {
let mut list = List::new();
list.calculate_visible_range(200.0);
assert_eq!(list.visible_range(), 0..0);
}
#[test]
fn test_list_get_item_size_variable() {
let items = vec![ListItem::new("1").size(30.0), ListItem::new("2").size(50.0)];
let mut list = List::new();
list.item_height = None;
list = list.items(items);
assert_eq!(list.content_size(), 80.0);
}
#[test]
fn test_list_key_boundary_checks() {
let items: Vec<_> = (0..3).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
list.focused_index = Some(0);
let _ = list.event(&Event::key_down(Key::Up));
assert_eq!(list.focused_index, Some(0));
list.focused_index = Some(2);
let _ = list.event(&Event::key_down(Key::Down));
assert_eq!(list.focused_index, Some(2)); }
#[test]
fn test_list_other_key_no_action() {
let items: Vec<_> = (0..3).map(|i| ListItem::new(format!("{i}"))).collect();
let mut list = List::new()
.selection_mode(SelectionMode::Single)
.item_height(50.0)
.items(items);
list.layout(Rect::new(0.0, 0.0, 300.0, 200.0));
list.focused_index = Some(1);
let result = list.event(&Event::key_down(Key::Tab));
assert!(result.is_none());
assert_eq!(list.focused_index, Some(1));
}
#[test]
fn test_list_brick_name() {
let list = List::new();
assert_eq!(list.brick_name(), "List");
}
#[test]
fn test_list_brick_assertions() {
let list = List::new();
let assertions = list.assertions();
assert!(!assertions.is_empty());
}
#[test]
fn test_list_brick_budget() {
let list = List::new();
let budget = list.budget();
assert!(budget.layout_ms > 0);
}
#[test]
fn test_list_brick_verify() {
let list = List::new();
let verification = list.verify();
assert!(!verification.passed.is_empty());
assert!(verification.failed.is_empty());
}
#[test]
fn test_list_brick_to_html() {
let list = List::new();
let html = list.to_html();
assert!(html.contains("brick-list"));
}
#[test]
fn test_list_brick_to_css() {
let list = List::new();
let css = list.to_css();
assert!(css.contains("brick-list"));
}
#[test]
fn test_list_brick_test_id() {
let list = List::new().with_test_id("test-list");
assert_eq!(Brick::test_id(&list), Some("test-list"));
}
}