database done + popup start

This commit is contained in:
maxstrb 2025-09-14 12:13:04 +02:00
commit 234b17a8b0
12 changed files with 1134 additions and 0 deletions

178
src/app.rs Normal file
View file

@ -0,0 +1,178 @@
use std::io;
use chrono::{Datelike, Local};
use crossterm::event::{self, Event, KeyEvent, KeyEventKind};
use ratatui::{
DefaultTerminal, Frame,
layout::{Constraint, Direction, Layout, Rect},
};
use crate::{
database::DB, days::Days, events::AppEvent, focused::Focused, month::Month, popup::Popup,
year::Year,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FocusedComponent {
Year,
Month,
Days,
DayPopup,
}
pub struct App {
year: Year,
month: Month,
days: Days,
day_popup: Popup,
focused: FocusedComponent,
exit: bool,
}
impl App {
pub fn new() -> Self {
let mut app = App {
year: Year::default(),
month: Month::default(),
days: Days::default(),
day_popup: Popup::default(),
focused: FocusedComponent::DayPopup,
exit: false,
};
app.days
.reload(app.month.month, app.year.year, Local::now().day() as i32);
app.get_focused_mut().take_focus();
app
}
fn get_focused_mut(&mut self) -> &mut dyn Focused {
match self.focused {
FocusedComponent::Year => &mut self.year,
FocusedComponent::Month => &mut self.month,
FocusedComponent::Days => &mut self.days,
FocusedComponent::DayPopup => &mut self.day_popup,
}
}
pub fn switch_focus(&mut self, new_focus: FocusedComponent) {
if self.focused != new_focus {
self.get_focused_mut().lose_focus();
self.focused = new_focus;
self.get_focused_mut().take_focus();
}
}
fn handle_input(&mut self, key_event: KeyEvent, db: &mut DB) -> Option<Vec<AppEvent>> {
let focused = self.get_focused_mut();
focused.handle_input(key_event, db)
}
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
let mut db = DB::new().unwrap();
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events(&mut db)?;
}
Ok(())
}
fn draw(&mut self, frame: &mut Frame) {
let main_area = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(8),
Constraint::Length(4),
Constraint::Fill(1),
])
.split(frame.area());
let popup_area = Self::popup_rect(50, 50, frame.area());
if frame.area().width < 10 || frame.area().height < 30 {
self.exit();
return;
}
self.year.ready_to_render(main_area[0]);
self.month.ready_to_render(main_area[1]);
self.days.ready_to_render(main_area[2]);
self.day_popup.ready_to_render(popup_area);
frame.render_widget(&self.year, main_area[0]);
frame.render_widget(&self.month, main_area[1]);
frame.render_widget(&self.days, main_area[2]);
frame.render_widget(&self.day_popup, popup_area);
}
fn popup_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}
fn exit(&mut self) {
self.exit = true;
}
fn handle_events(&mut self, db: &mut DB) -> io::Result<()> {
match event::read()? {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
let app_event_option = self.handle_input(key_event, db);
if let Some(app_event_vec) = app_event_option {
for app_event in app_event_vec {
match app_event {
AppEvent::SwitchFocus(new_focus) => self.switch_focus(new_focus),
AppEvent::Exit => self.exit(),
AppEvent::YearScrolled(dir) => match dir {
crate::events::Direction::Up => {
self.year.plus_year();
}
crate::events::Direction::Down => {
self.year.minus_year();
}
},
AppEvent::MonthScrolled(dir) => match dir {
crate::events::Direction::Up => {
self.month.plus_month();
}
crate::events::Direction::Down => {
self.month.minus_month();
}
},
AppEvent::MonthSet(month) => {
self.days.reload(month, self.year.year, 1);
}
AppEvent::YearSet(year) => {
self.days.reload(self.month.month, year, 1);
}
_ => (),
}
}
}
}
_ => {}
};
Ok(())
}
}

156
src/database.rs Normal file
View file

@ -0,0 +1,156 @@
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{env::home_dir, fs};
use crate::day_info::{DayInfo, Event};
use chrono::NaiveDate;
use redb::{Database, Error, ReadableDatabase, ReadableTable, TableDefinition};
const MAIN_TABLE: TableDefinition<NaiveDate, Vec<(u64, String)>> = TableDefinition::new("main"); // Vec<tag_id, event_description>
const TAG_TABLE: TableDefinition<u64, String> = TableDefinition::new("tags");
pub struct DB {
db: Database,
}
impl DB {
pub fn new() -> Result<Self, Error> {
let mut path = home_dir().ok_or(Error::DatabaseAlreadyOpen)?;
path.push(".local/share/cars");
fs::create_dir_all(&path)?;
path.push("main.redb");
let db = Database::create(path)?;
let write_txn = db.begin_write()?;
{
let _main_table = write_txn.open_table(MAIN_TABLE)?;
let _tag_table = write_txn.open_table(TAG_TABLE)?;
}
write_txn.commit()?;
Ok(Self { db })
}
pub fn get_day(&self, day: NaiveDate) -> Result<Option<DayInfo>, Error> {
match self.db.begin_read()?.open_table(MAIN_TABLE)?.get(day)? {
None => Ok(None),
Some(data) => {
let events = data.value();
let mut output = DayInfo::default();
for (tag_id, event_description) in events {
let tag_name = self.db.begin_read()?.open_table(TAG_TABLE)?.get(tag_id)?;
output.events.push(match tag_name {
Some(_name) => Event::new(Some(tag_id), event_description),
None => Event::new(None, event_description),
});
}
Ok(Some(output))
}
}
}
pub fn add_event(&mut self, day: NaiveDate, event: Event) -> Result<(), Error> {
let mut new_vec = match self.db.begin_read()?.open_table(MAIN_TABLE)?.get(day)? {
Some(d) => d.value(),
None => vec![],
};
new_vec.push((
event.tag.expect("You should pass a valid event"),
event.description,
));
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(MAIN_TABLE)?;
table.insert(day, new_vec)?;
}
write_txn.commit()?;
Ok(())
}
pub fn remove_event(&mut self, day: NaiveDate, index: usize) -> Result<(), Error> {
let mut old_vec = match self.db.begin_read()?.open_table(MAIN_TABLE)?.get(day)? {
Some(d) => d.value(),
None => vec![],
};
if old_vec.len() <= index {
return Ok(());
}
old_vec.remove(index);
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(MAIN_TABLE)?;
table.insert(day, old_vec)?;
}
write_txn.commit()?;
Ok(())
}
pub fn get_tags(&self) -> Result<Vec<(u64, String)>, Error> {
self.db
.begin_read()?
.open_table(TAG_TABLE)?
.iter()?
.map(|x| -> Result<(u64, String), Error> {
let val = x?;
Ok((val.0.value(), val.1.value()))
})
.collect()
}
pub fn get_tag_name(&self, id: u64) -> Result<Option<String>, Error> {
match self.db.begin_read()?.open_table(TAG_TABLE)?.get(id)? {
Some(name) => Ok(Some(name.value())),
None => Ok(None),
}
}
pub fn create_tag(&mut self, tag_name: String) -> Result<u64, Error> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
let mut hasher = DefaultHasher::new();
tag_name.hash(&mut hasher);
let tag_hash = hasher.finish();
let tag_id = now.wrapping_add(tag_hash);
let write_txn = self.db.begin_write()?;
{
let mut tag_table = write_txn.open_table(TAG_TABLE)?;
tag_table.insert(tag_id, tag_name)?;
}
write_txn.commit()?;
std::thread::sleep(std::time::Duration::from_micros(1));
Ok(tag_id)
}
pub fn delete_tag(&mut self, tag_id: u64) -> Result<(), Error> {
let write_txn = self.db.begin_write()?;
{
let mut table = write_txn.open_table(TAG_TABLE)?;
table.remove(tag_id)?;
}
write_txn.commit()?;
Ok(())
}
}

22
src/day_info.rs Normal file
View file

@ -0,0 +1,22 @@
#[derive(Default, Clone)]
pub struct DayInfo {
pub events: Vec<Event>,
}
#[derive(Default, Clone)]
pub struct Event {
pub tag: Option<u64>,
pub description: String,
}
impl Event {
pub fn new(tag: Option<u64>, description: String) -> Self {
Self { tag, description }
}
}
impl DayInfo {
pub fn new(events: Vec<Event>) -> Self {
Self { events }
}
}

295
src/days.rs Normal file
View file

@ -0,0 +1,295 @@
use crate::{
app::FocusedComponent, database::DB, day_info::DayInfo, events::AppEvent, focused::Focused,
};
use chrono::{Datelike, Duration, NaiveDate};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Margin, Rect},
style::{Color, Style},
symbols::border,
text::Line,
widgets::{Block, Widget},
};
use std::collections::VecDeque;
#[derive(Default)]
pub struct Days {
days: VecDeque<Day>,
state: DaysState,
focused: bool,
selected_day: NaiveDate,
pivot: Option<NaiveDate>,
date_of_first_day: i32,
current_month: i32,
current_year: i32,
}
#[derive(Default)]
enum DaysState {
#[default]
Main,
DaySelect,
}
enum CursorMovement {
Up,
Down,
}
impl Days {
pub fn ready_to_render(&mut self, area: Rect) {
let view_rect = area.inner(Margin::new(1, 1));
let number_of_days_shown = view_rect.height;
if number_of_days_shown == 0 {
return;
}
if self.days.is_empty() || self.days.len() > number_of_days_shown as usize {
self.reload(
(self.selected_day.month0() + 1) as i32,
self.selected_day.year_ce().1 as i32,
(self.selected_day.day0() + 1) as i32,
);
}
while self.days.len() != number_of_days_shown.into() {
let next_day = Day::new(
self.days
.iter()
.last()
.unwrap()
.day
.checked_add_days(chrono::Days::new(1))
.unwrap(),
);
if next_day.day.year() >= 10_000 {
break;
}
self.days.push_back(next_day);
}
while self.days.len() != number_of_days_shown.into() {
let next_day = Day::new(
self.days
.iter()
.last()
.unwrap()
.day
.checked_sub_days(chrono::Days::new(1))
.unwrap(),
);
if next_day.day.year() < 0 {
break;
}
self.days.push_front(next_day);
}
}
pub fn reload(&mut self, month: i32, year: i32, day: i32) {
self.current_month = month;
self.current_year = year;
self.days.clear();
self.days.push_front(Day::new(
NaiveDate::from_ymd_opt(year, month as u32, day as u32).unwrap(),
));
self.selected_day = self.days.iter().next().unwrap().day;
self.date_of_first_day = self.days.iter().next().unwrap().index();
}
fn handle_main_state_input(&mut self, key_code: KeyCode) -> Option<Vec<AppEvent>> {
match key_code {
KeyCode::Esc => Some(vec![AppEvent::Exit]),
KeyCode::Enter => {
self.state = DaysState::DaySelect;
None
}
KeyCode::Up => Some(vec![AppEvent::SwitchFocus(FocusedComponent::Month)]),
KeyCode::Down => {
self.state = DaysState::DaySelect;
self.handle_focused_arrows(CursorMovement::Down)
}
_ => None,
}
}
fn handle_focused_arrows(&mut self, cursor: CursorMovement) -> Option<Vec<AppEvent>> {
match cursor {
CursorMovement::Up => {
if self.selected_day == NaiveDate::from_ymd_opt(9999, 12, 31).unwrap() {
return None;
}
}
CursorMovement::Down => {
if self.selected_day == NaiveDate::from_ymd_opt(0, 1, 1).unwrap() {
return None;
}
}
}
let mut events = vec![];
let previous_day = self.selected_day;
self.selected_day += match cursor {
CursorMovement::Up => Duration::days(-1),
CursorMovement::Down => Duration::days(1),
};
if self.selected_day < self.days.iter().next().unwrap().day {
self.days.pop_back();
self.days.push_front(Day::new(self.selected_day));
}
if self.selected_day > self.days.iter().last().unwrap().day {
self.days.pop_front();
self.days.push_back(Day::new(self.selected_day));
}
if previous_day.month0() != self.selected_day.month0() {
events.push(AppEvent::MonthScrolled(match cursor {
CursorMovement::Up => crate::events::Direction::Down,
CursorMovement::Down => crate::events::Direction::Up,
}));
}
if previous_day.year_ce().1 != self.selected_day.year_ce().1 {
events.push(AppEvent::YearScrolled(match cursor {
CursorMovement::Up => crate::events::Direction::Down,
CursorMovement::Down => crate::events::Direction::Up,
}));
}
if events.is_empty() {
None
} else {
Some(events)
}
}
fn is_date_between(start: NaiveDate, end: Option<NaiveDate>, current: NaiveDate) -> bool {
if let Some(e) = end {
(start <= current && current <= e) || (e <= current && current <= start)
} else {
current == start
}
}
fn handle_day_select_state_input(&mut self, key_code: KeyCode) -> Option<Vec<AppEvent>> {
match key_code {
KeyCode::Esc => {
self.state = DaysState::Main;
None
}
KeyCode::Enter => None,
KeyCode::Down => self.handle_focused_arrows(CursorMovement::Down),
KeyCode::Up => self.handle_focused_arrows(CursorMovement::Up),
KeyCode::Char(' ') => {
self.pivot = match self.pivot {
None => Some(self.selected_day),
Some(_) => None,
};
None
}
_ => None,
}
}
}
impl Focused for Days {
fn take_focus(&mut self) {
self.focused = true;
}
fn lose_focus(&mut self) {
self.focused = false;
}
fn handle_input(&mut self, key_event: KeyEvent, _db: &mut DB) -> Option<Vec<AppEvent>> {
match self.state {
DaysState::Main => self.handle_main_state_input(key_event.code),
DaysState::DaySelect => self.handle_day_select_state_input(key_event.code),
}
}
}
impl Widget for &Days {
fn render(self, area: Rect, buf: &mut Buffer) {
let block = Block::bordered()
.border_set(border::ROUNDED)
.border_style(Style::default().fg(match self.focused {
true => Color::Blue,
false => Color::White,
}))
.title_alignment(ratatui::layout::Alignment::Center)
.title_bottom(" (T) to manage tags | (A) to add event | (Return) to show more ");
let inner_area = block.inner(area);
let constraints: Vec<Constraint> =
self.days.iter().map(|_| Constraint::Length(1)).collect();
if !constraints.is_empty() {
let day_areas = Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints(constraints)
.split(inner_area);
for (day, area) in self.days.iter().zip(day_areas.iter()) {
day.get_line(self.selected_day, self.pivot, self.date_of_first_day)
.render(*area, buf);
}
}
block.render(area, buf);
}
}
#[derive(Clone)]
pub struct Day {
pub day: NaiveDate,
pub info: DayInfo,
}
impl Day {
pub fn new(day: NaiveDate) -> Self {
Self {
day,
info: DayInfo::default(),
}
}
fn index(&self) -> i32 {
self.day
.signed_duration_since(NaiveDate::from_ymd_opt(0, 1, 1).unwrap())
.num_days() as i32
}
fn get_line(
&self,
current_day: NaiveDate,
pivot: Option<NaiveDate>,
date_of_first_day: i32,
) -> Line<'_> {
Line::styled(
format!(
"{}, {}, {}",
self.day.day(),
self.day.month(),
self.day.year()
),
Style::new().bg(if Days::is_date_between(current_day, pivot, self.day) {
Color::Red
} else if (self.index() - date_of_first_day).abs() % 2 == 0 {
Color::DarkGray
} else {
Color::Reset
}),
)
}
}

20
src/events.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::app::FocusedComponent;
use crate::day_info::DayInfo;
pub enum AppEvent {
SwitchFocus(FocusedComponent),
DaySelected(DayInfo),
YearSet(i32),
MonthSet(i32),
YearScrolled(Direction),
MonthScrolled(Direction),
Exit,
}
pub enum Direction {
Up,
Down,
}

10
src/focused.rs Normal file
View file

@ -0,0 +1,10 @@
use crossterm::event::KeyEvent;
use crate::database::DB;
use crate::events::AppEvent;
pub trait Focused {
fn take_focus(&mut self);
fn lose_focus(&mut self);
fn handle_input(&mut self, key_event: KeyEvent, db: &mut DB) -> Option<Vec<AppEvent>>;
}

0
src/global_shortcuts.rs Normal file
View file

20
src/joiner.rs Normal file
View file

@ -0,0 +1,20 @@
pub fn join_ascii_chars_functional(chars: &[&str]) -> String {
let char_lines: Vec<Vec<&str>> = chars.iter().map(|ch| ch.lines().collect()).collect();
if char_lines.is_empty() {
return String::new();
}
(0..char_lines[0].len())
.map(|row| {
char_lines
.iter()
.filter_map(|char_line| char_line.get(row))
.fold(String::new(), |mut acc, s| {
acc.push_str(s);
acc
})
})
.collect::<Vec<_>>()
.join("\n")
}

25
src/main.rs Normal file
View file

@ -0,0 +1,25 @@
use std::{/*env::args,*/ io};
mod app;
mod database;
mod day_info;
mod days;
mod events;
mod focused;
mod global_shortcuts;
mod joiner;
mod month;
mod popup;
mod year;
use app::App;
fn main() -> io::Result<()> {
let mut terminal = ratatui::init();
//let command_line_args: Vec<String> = args().collect();
let app_result = App::new().run(&mut terminal);
ratatui::restore();
app_result
}

146
src/month.rs Normal file
View file

@ -0,0 +1,146 @@
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols::border,
widgets::{Block, Paragraph, Widget},
};
use crate::{
app::FocusedComponent,
database::DB,
events::{AppEvent, Direction},
focused::Focused,
};
use chrono::{Datelike, Local};
const MONTHS: &[&str] = &[
"\
",
"\
",
"\
",
"\
",
"\
",
"\
",
"\
",
"\
",
"\
",
"\
",
"\
",
"\
",
];
pub struct Month {
pub month: i32,
focused: bool,
}
impl Default for Month {
fn default() -> Self {
Self {
month: Local::now().month() as i32,
focused: false,
}
}
}
impl Month {
pub fn ready_to_render(&mut self, _area: Rect) {}
pub fn plus_month(&mut self) {
self.month = (self.month % 12) + 1;
}
pub fn minus_month(&mut self) {
self.month = if self.month == 1 { 12 } else { self.month - 1 }
}
}
impl Focused for Month {
fn take_focus(&mut self) {
self.focused = true;
}
fn lose_focus(&mut self) {
self.focused = false;
}
fn handle_input(&mut self, key_event: KeyEvent, _db: &mut DB) -> Option<Vec<AppEvent>> {
match key_event.code {
KeyCode::Esc => Some(vec![AppEvent::Exit]),
KeyCode::Up => Some(vec![AppEvent::SwitchFocus(FocusedComponent::Year)]),
KeyCode::Down => Some(vec![AppEvent::SwitchFocus(FocusedComponent::Days)]),
KeyCode::Right => {
self.month += 1;
if self.month >= 13 {
self.month = 1;
Some(vec![
AppEvent::YearScrolled(Direction::Up),
AppEvent::MonthSet(self.month),
])
} else {
Some(vec![AppEvent::MonthSet(self.month)])
}
}
KeyCode::Left => {
self.month -= 1;
if self.month <= 0 {
self.month = 12;
Some(vec![
AppEvent::YearScrolled(Direction::Down),
AppEvent::MonthSet(self.month),
])
} else {
Some(vec![AppEvent::MonthSet(self.month)])
}
}
_ => None,
}
}
}
impl Widget for &Month {
fn render(self, area: Rect, buf: &mut Buffer) {
let block =
Block::bordered()
.border_set(border::ROUNDED)
.border_style(Style::default().fg(match self.focused {
true => Color::Blue,
false => Color::White,
}));
let bordered_area = block.inner(area);
block.render(area, buf);
Paragraph::new(MONTHS[(self.month - 1) as usize]).render(bordered_area, buf);
}
}

68
src/popup.rs Normal file
View file

@ -0,0 +1,68 @@
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols::border,
widgets::{Block, Clear, Widget},
};
use crate::{app::FocusedComponent, database::DB, database::*, events::AppEvent, focused::Focused};
pub enum PopupType {
TagManagement(Vec<(u64, String)>),
DayDetails,
EventAdding,
}
impl Default for PopupType {
fn default() -> Self {
PopupType::TagManagement(vec![])
}
}
#[derive(Default)]
pub struct Popup {
self_state: PopupType,
focused: bool,
}
impl Popup {
pub fn ready_to_render(&mut self, _area: Rect) {}
}
impl Focused for Popup {
fn take_focus(&mut self) {
self.focused = true;
}
fn lose_focus(&mut self) {
self.focused = false;
}
fn handle_input(&mut self, key_event: KeyEvent, _db: &mut DB) -> Option<Vec<AppEvent>> {
match key_event.code {
KeyCode::Esc => Some(vec![AppEvent::SwitchFocus(FocusedComponent::Days)]),
_ => None,
}
}
}
impl Widget for &Popup {
fn render(self, area: Rect, buf: &mut Buffer) {
if !self.focused {
return;
}
Clear.render(area, buf);
Block::bordered()
.border_set(border::ROUNDED)
.border_style(Style::default().fg(match self.focused {
true => Color::Blue,
false => Color::White,
}))
.title("DayPopup")
.render(area, buf);
}
}

194
src/year.rs Normal file
View file

@ -0,0 +1,194 @@
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols::border,
widgets::{Block, Paragraph, Widget},
};
use crate::{
app::FocusedComponent, database::DB, events::AppEvent, focused::Focused,
joiner::join_ascii_chars_functional,
};
use chrono::{Datelike, Local};
const DIGITS: &[&str] = &[
"\
",
" ╔╗
",
"\
",
"\
",
"\
",
"\
",
"\
",
"\
",
"\
",
"\
",
];
pub struct Year {
pub year: i32,
focused: bool,
}
impl Default for Year {
fn default() -> Self {
Self {
year: Local::now().year(),
focused: false,
}
}
}
impl Year {
pub fn ready_to_render(&mut self, _area: Rect) {}
pub fn month_overflow(&mut self) {
self.plus_year();
}
pub fn month_underflow(&mut self) {
self.minus_year();
}
pub fn plus_year(&mut self) -> bool {
if self.year < 10_000 {
self.year += 1;
true
} else {
false
}
}
pub fn minus_year(&mut self) -> bool {
if self.year > 0 {
self.year -= 1;
true
} else {
false
}
}
}
impl Focused for Year {
fn take_focus(&mut self) {
self.focused = true;
}
fn lose_focus(&mut self) {
self.focused = false;
}
fn handle_input(&mut self, key_event: KeyEvent, _db: &mut DB) -> Option<Vec<AppEvent>> {
match key_event.code {
KeyCode::Esc => Some(vec![AppEvent::Exit]),
KeyCode::Down => Some(vec![AppEvent::SwitchFocus(FocusedComponent::Month)]),
KeyCode::Right => {
if self.plus_year() {
Some(vec![AppEvent::YearSet(self.year)])
} else {
None
}
}
KeyCode::Left => {
if self.minus_year() {
Some(vec![AppEvent::YearSet(self.year)])
} else {
None
}
}
_ => None,
}
}
}
impl Widget for &Year {
fn render(self, area: Rect, buf: &mut Buffer) {
let block =
Block::bordered()
.border_set(border::ROUNDED)
.border_style(Style::default().fg(match self.focused {
true => Color::Blue,
false => Color::White,
}));
let bordered_area = block.inner(area);
block.render(area, buf);
let digit_one = (self.year - self.year % 1000) / 1000;
let digit_two = (self.year - digit_one * 1000 - self.year % 100) / 100;
let digit_four = self.year % 10;
let digit_three = ((self.year - digit_four) % 100) / 10;
let chars = vec![
DIGITS[digit_one as usize],
DIGITS[digit_two as usize],
DIGITS[digit_three as usize],
DIGITS[digit_four as usize],
];
Paragraph::new(join_ascii_chars_functional(&chars)).render(bordered_area, buf);
}
}