use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
};
#[derive(Debug, Clone, Default)]
pub struct MousePointerState {
pub enabled: bool,
pub position: Option<(u16, u16)>,
}
impl MousePointerState {
pub fn new() -> Self {
Self::default()
}
pub fn with_enabled(enabled: bool) -> Self {
Self {
enabled,
position: None,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn toggle(&mut self) {
self.enabled = !self.enabled;
}
pub fn update_position(&mut self, col: u16, row: u16) {
self.position = Some((col, row));
}
pub fn clear_position(&mut self) {
self.position = None;
}
pub fn should_render(&self) -> bool {
self.enabled && self.position.is_some()
}
}
#[derive(Debug, Clone)]
pub struct MousePointerStyle {
pub symbol: &'static str,
pub fg: Color,
pub bg: Option<Color>,
}
impl Default for MousePointerStyle {
fn default() -> Self {
Self {
symbol: "█",
fg: Color::Yellow,
bg: None,
}
}
}
impl From<&crate::theme::Theme> for MousePointerStyle {
fn from(theme: &crate::theme::Theme) -> Self {
let p = &theme.palette;
Self {
symbol: "█",
fg: p.primary,
bg: None,
}
}
}
impl MousePointerStyle {
pub fn crosshair() -> Self {
Self {
symbol: "┼",
fg: Color::Cyan,
bg: None,
}
}
pub fn arrow() -> Self {
Self {
symbol: "▶",
fg: Color::White,
bg: None,
}
}
pub fn dot() -> Self {
Self {
symbol: "●",
fg: Color::Green,
bg: None,
}
}
pub fn plus() -> Self {
Self {
symbol: "+",
fg: Color::Magenta,
bg: None,
}
}
pub fn custom(symbol: &'static str, fg: Color) -> Self {
Self {
symbol,
fg,
bg: None,
}
}
pub fn symbol(mut self, symbol: &'static str) -> Self {
self.symbol = symbol;
self
}
pub fn fg(mut self, fg: Color) -> Self {
self.fg = fg;
self
}
pub fn bg(mut self, bg: Color) -> Self {
self.bg = Some(bg);
self
}
}
#[derive(Debug, Clone)]
pub struct MousePointer<'a> {
state: &'a MousePointerState,
style: MousePointerStyle,
}
impl<'a> MousePointer<'a> {
pub fn new(state: &'a MousePointerState) -> Self {
Self {
state,
style: MousePointerStyle::default(),
}
}
pub fn style(mut self, style: MousePointerStyle) -> Self {
self.style = style;
self
}
pub fn theme(self, theme: &crate::theme::Theme) -> Self {
self.style(MousePointerStyle::from(theme))
}
pub fn render(self, buf: &mut Buffer) {
if !self.state.should_render() {
return;
}
let (col, row) = self.state.position.unwrap();
self.render_at(buf, col, row);
}
pub fn render_in_area(self, buf: &mut Buffer, area: Rect) {
if !self.state.should_render() {
return;
}
let (col, row) = self.state.position.unwrap();
if col >= area.x && col < area.x + area.width && row >= area.y && row < area.y + area.height
{
self.render_at(buf, col, row);
}
}
fn render_at(&self, buf: &mut Buffer, col: u16, row: u16) {
let buf_area = buf.area();
if col >= buf_area.x + buf_area.width || row >= buf_area.y + buf_area.height {
return;
}
let mut cell_style = Style::default().fg(self.style.fg);
if let Some(bg) = self.style.bg {
cell_style = cell_style.bg(bg);
}
buf[(col, row)]
.set_symbol(self.style.symbol)
.set_style(cell_style);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_state_default() {
let state = MousePointerState::default();
assert!(!state.enabled);
assert!(state.position.is_none());
assert!(!state.should_render());
}
#[test]
fn test_state_with_enabled() {
let state = MousePointerState::with_enabled(true);
assert!(state.enabled);
assert!(state.position.is_none());
assert!(!state.should_render()); }
#[test]
fn test_state_toggle() {
let mut state = MousePointerState::default();
assert!(!state.enabled);
state.toggle();
assert!(state.enabled);
state.toggle();
assert!(!state.enabled);
}
#[test]
fn test_state_position_update() {
let mut state = MousePointerState::default();
state.set_enabled(true);
assert!(state.position.is_none());
state.update_position(10, 5);
assert_eq!(state.position, Some((10, 5)));
assert!(state.should_render());
state.clear_position();
assert!(state.position.is_none());
assert!(!state.should_render());
}
#[test]
fn test_should_render() {
let mut state = MousePointerState::default();
assert!(!state.should_render());
state.set_enabled(true);
assert!(!state.should_render());
state.update_position(5, 5);
assert!(state.should_render());
state.set_enabled(false);
assert!(!state.should_render());
}
#[test]
fn test_style_default() {
let style = MousePointerStyle::default();
assert_eq!(style.symbol, "█");
assert_eq!(style.fg, Color::Yellow);
assert!(style.bg.is_none());
}
#[test]
fn test_style_presets() {
let crosshair = MousePointerStyle::crosshair();
assert_eq!(crosshair.symbol, "┼");
assert_eq!(crosshair.fg, Color::Cyan);
let arrow = MousePointerStyle::arrow();
assert_eq!(arrow.symbol, "▶");
assert_eq!(arrow.fg, Color::White);
let dot = MousePointerStyle::dot();
assert_eq!(dot.symbol, "●");
assert_eq!(dot.fg, Color::Green);
let plus = MousePointerStyle::plus();
assert_eq!(plus.symbol, "+");
assert_eq!(plus.fg, Color::Magenta);
}
#[test]
fn test_style_custom() {
let custom = MousePointerStyle::custom("X", Color::Red);
assert_eq!(custom.symbol, "X");
assert_eq!(custom.fg, Color::Red);
}
#[test]
fn test_style_builder() {
let style = MousePointerStyle::default()
.symbol("*")
.fg(Color::Blue)
.bg(Color::White);
assert_eq!(style.symbol, "*");
assert_eq!(style.fg, Color::Blue);
assert_eq!(style.bg, Some(Color::White));
}
#[test]
fn test_render_disabled() {
let state = MousePointerState::default();
let pointer = MousePointer::new(&state);
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
pointer.render(&mut buf);
for y in 0..10 {
for x in 0..10 {
assert_eq!(buf[(x, y)].symbol(), " ");
}
}
}
#[test]
fn test_render_enabled() {
let mut state = MousePointerState::default();
state.set_enabled(true);
state.update_position(5, 5);
let pointer = MousePointer::new(&state);
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
pointer.render(&mut buf);
assert_eq!(buf[(5, 5)].symbol(), "█");
}
#[test]
fn test_render_with_custom_style() {
let mut state = MousePointerState::default();
state.set_enabled(true);
state.update_position(3, 3);
let pointer = MousePointer::new(&state).style(MousePointerStyle::crosshair());
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
pointer.render(&mut buf);
assert_eq!(buf[(3, 3)].symbol(), "┼");
}
#[test]
fn test_render_out_of_bounds() {
let mut state = MousePointerState::default();
state.set_enabled(true);
state.update_position(100, 100);
let pointer = MousePointer::new(&state);
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
pointer.render(&mut buf);
for y in 0..10 {
for x in 0..10 {
assert_eq!(buf[(x, y)].symbol(), " ");
}
}
}
#[test]
fn test_render_in_area_inside() {
let mut state = MousePointerState::default();
state.set_enabled(true);
state.update_position(5, 5);
let pointer = MousePointer::new(&state);
let area = Rect::new(0, 0, 10, 10);
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 20));
pointer.render_in_area(&mut buf, area);
assert_eq!(buf[(5, 5)].symbol(), "█");
}
#[test]
fn test_render_in_area_outside() {
let mut state = MousePointerState::default();
state.set_enabled(true);
state.update_position(15, 15);
let pointer = MousePointer::new(&state);
let area = Rect::new(0, 0, 10, 10);
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 20));
pointer.render_in_area(&mut buf, area);
assert_eq!(buf[(15, 15)].symbol(), " ");
}
#[test]
fn test_render_at_boundary() {
let mut state = MousePointerState::default();
state.set_enabled(true);
state.update_position(9, 9);
let pointer = MousePointer::new(&state);
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
pointer.render(&mut buf);
assert_eq!(buf[(9, 9)].symbol(), "█");
}
}