use crate::core::Color;
use crate::core::{Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
pub struct GridWidget {
base: BaseWidget,
rows: u32,
columns: u32,
spacing: u32,
line_color: Option<Color>,
cell_width: u32,
cell_height: u32,
hovered_cell: Option<(u32, u32)>,
pub cell_clicked: Signal1<(u32, u32)>,
pub cell_hovered: Signal1<(u32, u32)>,
}
impl GridWidget {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Grid, geometry, "GridWidget"),
rows: 1,
columns: 1,
spacing: 0,
line_color: Some(Color::from_rgb(220, 220, 220)),
cell_width: geometry.width,
cell_height: geometry.height,
hovered_cell: None,
cell_clicked: Signal1::new(),
cell_hovered: Signal1::new(),
}
}
pub fn with_dimensions(geometry: Rect, rows: u32, columns: u32) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Grid, geometry, "GridWidget"),
rows: rows.max(1),
columns: columns.max(1),
spacing: 0,
line_color: Some(Color::from_rgb(220, 220, 220)),
cell_width: geometry.width / columns.max(1),
cell_height: geometry.height / rows.max(1),
hovered_cell: None,
cell_clicked: Signal1::new(),
cell_hovered: Signal1::new(),
}
}
pub fn rows(&self) -> u32 {
self.rows
}
pub fn set_rows(&mut self, rows: u32) {
self.rows = rows.max(1);
self.update_cell_dimensions();
self.base.request_redraw();
}
pub fn columns(&self) -> u32 {
self.columns
}
pub fn set_columns(&mut self, columns: u32) {
self.columns = columns.max(1);
self.update_cell_dimensions();
self.base.request_redraw();
}
pub fn spacing(&self) -> u32 {
self.spacing
}
pub fn set_spacing(&mut self, spacing: u32) {
self.spacing = spacing;
self.update_cell_dimensions();
self.base.request_redraw();
}
pub fn line_color(&self) -> Option<Color> {
self.line_color
}
pub fn set_line_color(&mut self, color: Option<Color>) {
self.line_color = color;
self.base.request_redraw();
}
pub fn cell_width(&self) -> u32 {
self.cell_width
}
pub fn cell_height(&self) -> u32 {
self.cell_height
}
pub fn cell_at_y(&self, y: i32) -> Option<u32> {
let rect = self.base.geometry();
if y < rect.y || y >= rect.y + rect.height as i32 {
return None;
}
if self.rows == 0 || self.cell_height == 0 {
return None;
}
let local_y = (y - rect.y) as u32;
let row = local_y / (self.cell_height + self.spacing);
if row < self.rows {
Some(row)
} else {
None
}
}
pub fn cell_at_x(&self, x: i32) -> Option<u32> {
let rect = self.base.geometry();
if x < rect.x || x >= rect.x + rect.width as i32 {
return None;
}
if self.columns == 0 || self.cell_width == 0 {
return None;
}
let local_x = (x - rect.x) as u32;
let col = local_x / (self.cell_width + self.spacing);
if col < self.columns {
Some(col)
} else {
None
}
}
pub fn cell_at(&self, point: Point) -> Option<(u32, u32)> {
let row = self.cell_at_y(point.y)?;
let col = self.cell_at_x(point.x)?;
Some((row, col))
}
pub fn cell_rect(&self, row: u32, col: u32) -> Option<Rect> {
if row >= self.rows || col >= self.columns {
return None;
}
let rect = self.base.geometry();
let x = rect.x + (col * (self.cell_width + self.spacing)) as i32;
let y = rect.y + (row * (self.cell_height + self.spacing)) as i32;
Some(Rect::new(x, y, self.cell_width, self.cell_height))
}
fn update_cell_dimensions(&mut self) {
let rect = self.base.geometry();
let total_spacing_w = self.spacing.saturating_mul(self.columns.saturating_sub(1));
let total_spacing_h = self.spacing.saturating_mul(self.rows.saturating_sub(1));
self.cell_width = (rect.width.saturating_sub(total_spacing_w)) / self.columns;
self.cell_height = (rect.height.saturating_sub(total_spacing_h)) / self.rows;
}
}
impl Widget for GridWidget {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
let w = self.columns * 20 + self.spacing.saturating_mul(self.columns.saturating_sub(1));
let h = self.rows * 20 + self.spacing.saturating_mul(self.rows.saturating_sub(1));
Size::new(w.max(40), h.max(40))
}
}
impl Draw for GridWidget {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.base.geometry();
self.update_cell_dimensions();
context.fill_rect(rect, Color::from_rgb(250, 250, 252));
context.draw_rect(rect, Color::from_rgb(180, 185, 195));
let Some(line_color) = self.line_color else {
return;
};
if self.rows <= 1 && self.columns <= 1 {
return;
}
let total_w = self.columns * self.cell_width
+ self.spacing.saturating_mul(self.columns.saturating_sub(1));
let total_h =
self.rows * self.cell_height + self.spacing.saturating_mul(self.rows.saturating_sub(1));
for col in 1..self.columns {
let x = rect.x + (col * (self.cell_width + self.spacing)) as i32
- (self.spacing / 2) as i32;
let x = x.max(rect.x).min(rect.x + total_w as i32);
context.draw_line(
Point::new(x, rect.y),
Point::new(x, rect.y + total_h as i32),
line_color,
);
}
for row in 1..self.rows {
let y = rect.y + (row * (self.cell_height + self.spacing)) as i32
- (self.spacing / 2) as i32;
let y = y.max(rect.y).min(rect.y + total_h as i32);
context.draw_line(
Point::new(rect.x, y),
Point::new(rect.x + total_w as i32, y),
line_color,
);
}
}
}
impl EventHandler for GridWidget {
fn handle_event(&mut self, event: &crate::event::Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match *event {
Event::MouseMove { pos } => {
if let Some(cell) = self.cell_at(pos) {
if self.hovered_cell != Some(cell) {
self.hovered_cell = Some(cell);
self.cell_hovered.emit(cell);
}
} else {
self.hovered_cell = None;
}
}
Event::MousePress { pos, button: 1 } => {
self.base.set_mouse_pressed(true);
if let Some(cell) = self.cell_at(pos) {
self.base.clicked.emit();
self.cell_clicked.emit(cell);
}
}
Event::MouseRelease { pos: _, button: 1 } => {
self.base.set_mouse_pressed(false);
}
_ => { }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[test]
fn with_dimensions_uses_columns_for_width_and_rows_for_height() {
let grid = GridWidget::with_dimensions(Rect::new(0, 0, 120, 80), 2, 4);
assert_eq!(grid.cell_width(), 30);
assert_eq!(grid.cell_height(), 40);
}
#[test]
fn grid_mouse_interaction_emits_cell_signals() {
let mut grid = GridWidget::with_dimensions(Rect::new(0, 0, 100, 100), 2, 2);
let clicked = Arc::new(Mutex::new(Vec::<(u32, u32)>::new()));
let hovered = Arc::new(Mutex::new(Vec::<(u32, u32)>::new()));
let clicked_sink = clicked.clone();
grid.cell_clicked.connect(move |cell| {
if let Ok(mut guard) = clicked_sink.lock() {
guard.push(*cell);
}
});
let hovered_sink = hovered.clone();
grid.cell_hovered.connect(move |cell| {
if let Ok(mut guard) = hovered_sink.lock() {
guard.push(*cell);
}
});
grid.handle_event(&Event::mouse_move(75, 25));
grid.handle_event(&Event::mouse_press(75, 25, 1));
let hovered_values = hovered.lock().expect("hovered lock poisoned").clone();
let clicked_values = clicked.lock().expect("clicked lock poisoned").clone();
assert_eq!(hovered_values, vec![(0, 1)]);
assert_eq!(clicked_values, vec![(0, 1)]);
}
}