use std::sync::{Arc, Mutex}; use ratatui::{layout::{Constraint, Direction, Layout, Rect}, style::{palette::tailwind::SLATE, Color, Modifier, Style}, text::{Line, Span, Text}, widgets::{canvas::{Canvas, Circle, Map, MapResolution, Points}, Block, Borders, List, ListItem, Paragraph}, Frame}; use ratatui::prelude::Stylize; use crate::{App, CurrentScreen}; const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD); pub fn ui(frame: &mut Frame, app: &mut Arc>) { let app_shared = Arc::clone(&app); let mut app_shared = app_shared.lock().unwrap(); let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), Constraint::Min(1), Constraint::Length(3), ]) .split(frame.area()); let title_block = Block::default() .borders(Borders::ALL) .style(Style::default()); let title = Paragraph::new(Text::styled( "ISS Locator", Style::default().fg(ratatui::style::Color::Green), )) .block(title_block); frame.render_widget(title, chunks[0]); // MAP let map = Canvas::default() .block(Block::bordered().title("World Map")) .marker(ratatui::symbols::Marker::Braille) .paint(|ctx| { ctx.draw(&Map { color: Color::Green, resolution: MapResolution::High, }); if let Some(loc) = &app_shared.iss_location { //allegedly the area visible from the ISS but we all know its not, there is a way //to create a new widget that, depending on input location would give the //appropriate shape, I however didn't care enough yet to do so. ctx.draw(&Circle { color: Color::Blue, radius: 13.0, x: loc.longitude.into(), y: loc.latitude.into() }); } for (i,city) in app_shared.cities.cities.iter().enumerate() { ctx.draw(&Points { coords: &[(city.long.into(),city.lat.into())], color: Color::Red }); if let Some(j) = app_shared.cities.state.selected(){ if i == j { // ctx.draw(&Points { // coords: &[(city.long.into(),city.lat.into())], // color: Color::Blue // }); ctx.draw(&Circle { color: Color::Yellow, radius: 0.7, x: city.long.into(), y: city.lat.into() }); }} } }) .x_bounds([-180.0, 180.0]) .y_bounds([-90.0, 90.0]); frame.render_widget(map, chunks[1]); // text on the bottom // let nav_text = vec![ match app_shared.current_screen { CurrentScreen::Map => Span::styled("Map", Style::default().fg(Color::Green)), CurrentScreen::CityChoice => Span::styled("Location Choice", Style::default().fg(Color::Green)), CurrentScreen::Exiting => Span::styled("Exiting", Style::default().fg(Color::Green)) }.to_owned(), Span::styled(" | ", Style::default().fg(Color::White)), Span::styled(format!("Current location: {}",{ if let Some(i) = app_shared.cities.state.selected() { &app_shared.cities.cities[i].name } else { "Not selected" } }), Style::default().fg(Color::Green)), Span::styled(" | ", Style::default().fg(Color::White)), Span::styled(format!("iss location: {} {} ",{ if let Some(i) = &app_shared.iss_location { i.latitude.clone().to_string() } else { "?".to_string() }}, { if let Some(i) = &app_shared.iss_location { i.longitude.clone().to_string() } else { "?".to_string() } } ), Style::default().fg(Color::Green)), Span::styled(" | ", Style::default().fg(Color::White)), Span::styled(format!(" {}",{ if app_shared.iss_constant.is_some() { "Y" } else { "N" } }), Style::default().fg(Color::Green)) ]; let mode_footer = Paragraph::new(Line::from(nav_text)) .block(Block::default().borders(Borders::ALL)); let curr_key_hint = { match app_shared.current_screen { CurrentScreen::Map => Span::styled( "(q) to quit / (c) to change the location", Style::default().fg(Color::Red)), CurrentScreen::CityChoice => Span::styled( "(ESC) to cancel / (Enter) to confirm choive", Style::default().fg(Color::Red)), CurrentScreen::Exiting => Span::styled( "(y) to quit / (ESC) to cancel" ,Style::default().fg(Color::Red)), } }; let hint_footer = Paragraph::new(Line::from(curr_key_hint)).block(Block::default().borders(Borders::ALL)); let footer_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50),Constraint::Percentage(50)]) .split(chunks[2]); frame.render_widget(mode_footer, footer_chunks[0]); frame.render_widget(hint_footer, footer_chunks[1]); // probably better to rewrite as a match statement if let CurrentScreen::CityChoice = app_shared.current_screen { let popup_block = Block::new() .title(Line::raw("Cities Option").centered()) .borders(Borders::ALL); let items: Vec = app_shared.cities.cities.iter() .map(|item| { ListItem::from(item) }) .collect(); let list = List::new(items) .block(popup_block) .style(SELECTED_STYLE) .highlight_symbol(">") .highlight_spacing(ratatui::widgets::HighlightSpacing::Always); let area = centered_rect(40, 40, frame.area()); frame.render_stateful_widget(list, area,&mut app_shared.cities.state); } if let CurrentScreen::Exiting = app_shared.current_screen { let exit_popup = Block::new() .title(Line::raw("Quit?").centered()) .bg(Color::Gray) .borders(Borders::ALL); let exit_text = Text::styled( "Would you like to leave?", Style::default().fg(Color::Red)); let exit_widget = Paragraph::new(exit_text).centered() .block(exit_popup); let area = centered_rect(30, 20, frame.area()); frame.render_widget(exit_widget, area); } } fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { // Cut the given rectangle into three vertical pieces 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); // Then cut the middle vertical piece into three width-wise pieces 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] // Return the middle chunk }