use ratatui::buffer::Buffer;
use super::set_cell;
use crate::primitives::date_picker::{DatePicker, DatePickerLayout};
use crate::theme::Theme;
fn fill_text(
buf: &mut Buffer,
rect: crate::event::Rect,
text: &str,
fg: ratatui::style::Color,
bg: ratatui::style::Color,
) {
fill_text_aligned(buf, rect, text, fg, bg, false);
}
fn fill_text_aligned(
buf: &mut Buffer,
rect: crate::event::Rect,
text: &str,
fg: ratatui::style::Color,
bg: ratatui::style::Color,
centered: bool,
) {
let x0 = rect.x.round() as u16;
let y0 = rect.y.round() as u16;
let w = rect.width.round() as u16;
let h = rect.height.round() as u16;
for row in 0..h {
for col in 0..w {
set_cell(
buf,
x0.saturating_add(col),
y0.saturating_add(row),
' ',
fg,
bg,
);
}
}
let len = text.chars().count() as u16;
let start = if centered && len < w {
(w - len) / 2
} else {
0
};
for (i, ch) in text
.chars()
.take(w.saturating_sub(start) as usize)
.enumerate()
{
set_cell(
buf,
x0.saturating_add(start).saturating_add(i as u16),
y0,
ch,
fg,
bg,
);
}
}
fn stroke_corners(
buf: &mut Buffer,
rect: crate::event::Rect,
fg: ratatui::style::Color,
bg: ratatui::style::Color,
) {
let x0 = rect.x.round() as u16;
let y0 = rect.y.round() as u16;
let w = rect.width.round() as u16;
let h = rect.height.round() as u16;
if w == 0 || h == 0 {
return;
}
let x1 = x0.saturating_add(w.saturating_sub(1));
let y1 = y0.saturating_add(h.saturating_sub(1));
set_cell(buf, x0, y0, '┌', fg, bg);
if x1 != x0 {
set_cell(buf, x1, y0, '┐', fg, bg);
}
if y1 != y0 {
set_cell(buf, x0, y1, '└', fg, bg);
if x1 != x0 {
set_cell(buf, x1, y1, '┘', fg, bg);
}
}
}
fn fill_text_with_ring(
buf: &mut Buffer,
rect: crate::event::Rect,
text: &str,
fg: ratatui::style::Color,
bg: ratatui::style::Color,
ring: Option<ratatui::style::Color>,
) {
let x0 = rect.x.round() as u16;
let y0 = rect.y.round() as u16;
let w = rect.width.round() as u16;
let h = rect.height.round() as u16;
for row in 0..h {
for col in 0..w {
set_cell(
buf,
x0.saturating_add(col),
y0.saturating_add(row),
' ',
fg,
bg,
);
}
}
let len = text.chars().count() as u16;
let has_inset_ring = ring.is_some() && w >= 3 && w >= len.saturating_add(2);
let text_start = u16::from(has_inset_ring);
let text_w = if has_inset_ring { w - 2 } else { w };
for (i, ch) in text.chars().take(text_w as usize).enumerate() {
set_cell(
buf,
x0.saturating_add(text_start).saturating_add(i as u16),
y0,
ch,
fg,
bg,
);
}
if let Some(ring_fg) = ring {
if has_inset_ring {
stroke_corners(buf, rect, ring_fg, bg);
} else if w > 0 && h > 0 {
for (i, ch) in text.chars().take(w as usize).enumerate() {
set_cell(buf, x0.saturating_add(i as u16), y0, ch, bg, ring_fg);
}
}
}
}
pub fn draw_date_picker(
buf: &mut Buffer,
date_picker: &DatePicker,
layout: &DatePickerLayout,
theme: &Theme,
) {
let bg = ratatui::style::Color::Reset;
let header_fg = super::qc(theme.header_fg);
let header_bg = super::qc(theme.header_bg);
if let Some(header) = layout.header_bounds {
if header.width > 0.0 && header.height > 0.0 {
let label = format!(
"{:04}-{:02}",
date_picker.visible_month.year, date_picker.visible_month.month
);
fill_text_aligned(buf, header, &label, header_fg, header_bg, true);
}
}
if let Some(prev) = layout.prev_bounds {
if prev.width > 0.0 && prev.height > 0.0 {
fill_text(buf, prev, "<", header_fg, header_bg);
}
}
if let Some(next) = layout.next_bounds {
if next.width > 0.0 && next.height > 0.0 {
fill_text(buf, next, ">", header_fg, header_bg);
}
}
let selected_bg = super::qc(theme.selected_bg);
let accent_fg = super::qc(theme.accent_fg);
let surface_fg = super::qc(theme.surface_fg);
let muted_fg = super::qc(theme.muted_fg);
for cell in &layout.day_cells {
if cell.bounds.width <= 0.0 || cell.bounds.height <= 0.0 {
continue;
}
let cell_bg = if cell.is_selected { selected_bg } else { bg };
let fg = if cell.in_visible_month {
surface_fg
} else {
muted_fg
};
let ring = cell.is_focused.then_some(accent_fg);
fill_text_with_ring(
buf,
cell.bounds,
&cell.date.day.to_string(),
fg,
cell_bg,
ring,
);
}
if let Some(time) = layout.time_bounds {
if time.width > 0.0 && time.height > 0.0 {
let input_bg = super::qc(theme.input_bg);
let border_fg = super::qc(theme.border_fg);
let text = date_picker.time.map(|t| t.format()).unwrap_or_default();
fill_text_with_ring(buf, time, &text, surface_fg, input_bg, Some(border_fg));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Rect;
use crate::primitives::date_picker::{CalendarDate, DatePickerHit};
use crate::types::WidgetId;
use ratatui::layout::Rect as RRect;
fn cell_char(buf: &Buffer, x: u16, y: u16) -> char {
buf[(x, y)].symbol().chars().next().unwrap_or(' ')
}
fn mk_date_picker() -> DatePicker {
let mut picker = DatePicker::new(WidgetId::new("date"), CalendarDate::new(2024, 3, 15));
picker.has_focus = true;
picker
}
fn paint_and_click_round_trip_at(origin_x: u16, origin_y: u16) {
let date_picker = mk_date_picker();
let layout = date_picker.layout(origin_x as f32, origin_y as f32, 4.0, 1.0, 1.0, 0.0);
let rect = RRect::new(0, 0, 80, 24);
let mut buf = Buffer::empty(rect);
draw_date_picker(&mut buf, &date_picker, &layout, &Theme::default());
let header = layout.header_bounds.expect("header is reserved");
let label_len = 7u16; let label_x = header.x as u16 + (header.width as u16 - label_len) / 2;
assert_eq!(cell_char(&buf, label_x, origin_y), '2');
assert_eq!(cell_char(&buf, origin_x, origin_y), '<');
let focused_cell = layout
.day_cells
.iter()
.find(|c| c.is_focused)
.expect("focused day is in the grid");
let fx = focused_cell.bounds.x as u16;
let fy = focused_cell.bounds.y as u16;
assert_eq!(cell_char(&buf, fx, fy), '┌');
for (i, ch) in focused_cell.date.day.to_string().chars().enumerate() {
assert_eq!(cell_char(&buf, fx + 1 + i as u16, fy), ch);
}
let p = Rect::new(focused_cell.bounds.x + 1.0, focused_cell.bounds.y, 0.0, 0.0);
assert_eq!(
layout.hit_test(p.x, p.y),
DatePickerHit::Day(focused_cell.date)
);
}
#[test]
fn paint_and_click_round_trip() {
paint_and_click_round_trip_at(0, 0);
}
#[test]
fn paint_and_click_round_trip_at_nonzero_origin() {
paint_and_click_round_trip_at(7, 3);
}
}