use color_eyre::Result; use crossterm::event::KeyEvent; use ratatui::layout::{Constraint, Margin}; use ratatui::widgets::{Padding, Scrollbar, ScrollbarOrientation}; use ratatui::{ text::Text, layout::{Alignment, Rect}, widgets::{Block, ScrollbarState, Cell, Row, Table, TableState}, Frame }; use subxt::utils::H256; use super::{Component, CurrentTab}; use crate::{ components::generic::{Activatable, Scrollable, PartialComponent}, action::Action, config::Config, palette::StylePalette, }; #[derive(Debug, Default)] struct BlockInfo { block_number: u32, finalized: bool, } pub struct BlockExplorer { is_active: bool, blocks: std::collections::VecDeque, block_headers: std::collections::HashMap, block_authors: std::collections::HashMap, scroll_state: ScrollbarState, table_state: TableState, palette: StylePalette, } impl Default for BlockExplorer { fn default() -> Self { Self::new() } } impl Activatable for BlockExplorer { fn is_active(&self) -> bool { self.is_active } fn is_inactive(&self) -> bool { !self.is_active } fn set_inactive(&mut self) { self.is_active = false; } fn set_active(&mut self) { self.is_active = true; } } impl Scrollable for BlockExplorer { type IndexType = usize; fn selected_index(&self) -> Option { self.table_state.selected() } fn items_length(&self) -> Self::IndexType { self.blocks.len() } fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result> { self.table_state.select(Some(new_index)); self.scroll_state = self.scroll_state.position(new_index); self.send_used_explorer_block(new_index) } fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result> { self.apply_next_row(new_index) } fn apply_first_row(&mut self) -> Result> { match self.items_length() > 0 { true => self.apply_next_row(0), false => Ok(None), } } fn apply_last_row(&mut self) -> Result> { match self.items_length().checked_sub(1) { Some(last_idx) => self.apply_next_row(last_idx), None => Ok(None), } } } impl BlockExplorer { const MAX_BLOCKS: usize = 50; pub fn new() -> Self { Self { is_active: false, blocks: Default::default(), block_authors: Default::default(), block_headers: Default::default(), scroll_state: ScrollbarState::new(0), table_state: TableState::new(), palette: StylePalette::default(), } } fn update_latest_block_info( &mut self, hash: H256, block_number: u32, ) -> Result> { let front_block_number = self.blocks .front() .map(|block| block.block_number) .unwrap_or_default(); if front_block_number < block_number { self.blocks.push_front(BlockInfo { block_number, finalized: false, }); self.block_headers.insert(block_number, hash); if self.items_length() > Self::MAX_BLOCKS { if let Some(block) = self.blocks.pop_back() { if let Some(hash) = self.block_headers.remove(&block.block_number) { self.block_authors.remove(&hash); } } } self.scroll_state = self.scroll_state.content_length(self.items_length()); return match self.table_state.selected() { Some(_) => self.next_row(), None => Ok(None), } } Ok(None) } fn update_finalized_block_info( &mut self, header: H256, block_number: u32, ) -> Result> { for idx in 0..self.items_length() { if self.blocks[idx].finalized { break; } else if self.blocks[idx].block_number > block_number { continue; } else { self.block_headers.insert(block_number, header); self.blocks[idx].finalized = true; } } Ok(None) } fn send_used_explorer_block(&mut self, index: usize) -> Result> { let maybe_block_number = self.blocks.get(index).map(|info| info.block_number); Ok(Some(Action::UsedExplorerBlock(maybe_block_number))) } } impl PartialComponent for BlockExplorer { fn set_active_tab(&mut self, current_tab: CurrentTab) { match current_tab { CurrentTab::Blocks => self.set_active(), CurrentTab::Extrinsics => self.set_inactive(), CurrentTab::Nothing => { self.set_inactive(); self.table_state.select(None); self.scroll_state = self.scroll_state.position(0); }, } } } impl Component for BlockExplorer { fn register_config_handler(&mut self, config: Config) -> Result<()> { if let Some(style) = config.styles.get(&crate::app::Mode::Explorer) { self.palette.with_normal_style(style.get("normal_style").copied()); self.palette.with_hover_style(style.get("hover_style").copied()); self.palette.with_normal_border_style(style.get("normal_border_style").copied()); self.palette.with_hover_border_style(style.get("hover_border_style").copied()); self.palette.with_normal_title_style(style.get("normal_title_style").copied()); self.palette.with_hover_title_style(style.get("hover_title_style").copied()); self.palette.with_highlight_style(style.get("highlight_style").copied()); self.palette.with_scrollbar_style(style.get("scrollbar_style").copied()); } Ok(()) } fn update(&mut self, action: Action) -> Result> { match action { Action::BestBlockInformation(header, block_number) => self.update_latest_block_info(header, block_number), Action::FinalizedBlockInformation(header, block_number) => self.update_finalized_block_info(header, block_number), Action::SetBlockAuthor(header, author) => { let _ = self.block_authors.insert(header, author); Ok(None) }, _ => Ok(None), } } fn handle_key_event(&mut self, key: KeyEvent) -> Result> { match self.is_active() { true => self.handle_scrollable_key_codes(key.code), false => Ok(None), } } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let [place, _] = super::layouts::explorer_scrollbars_layout(area); let (border_style, border_type) = self.palette.create_border_style(self.is_active()); let default_hash = H256::repeat_byte(69u8); let default_author = "...".to_string(); let rows = self.blocks .iter() .map(|info| { let header = self.block_headers .get(&info.block_number) .unwrap_or(&default_hash); let author = self.block_authors .get(&header) .unwrap_or(&default_author); if info.finalized { Row::new(vec![ Cell::from(Text::from(info.block_number.to_string()).alignment(Alignment::Left)), Cell::from(Text::from(header.to_string()).alignment(Alignment::Center)), Cell::from(Text::from(author.clone()).alignment(Alignment::Right)), ]).style(self.palette.create_highlight_style()) } else { Row::new(vec![ Cell::from(Text::from(info.block_number.to_string()).alignment(Alignment::Left)), Cell::from(Text::from(header.to_string()).alignment(Alignment::Center)), Cell::from(Text::from(author.clone()).alignment(Alignment::Right)), ]) } }) .collect::>(); let max_block_number_length = self.blocks .front() .map(|block| block.block_number) .unwrap_or_default() .checked_ilog10() .unwrap_or(0) as u16 + 1; let table = Table::new( rows, [ Constraint::Max(max_block_number_length + 2), Constraint::Min(15), Constraint::Min(0), ], ) .style(self.palette.create_basic_style(false)) .highlight_style(self.palette.create_basic_style(true)) .column_spacing(1) .block(Block::bordered() .border_style(border_style) .border_type(border_type) .padding(Padding::right(2)) .title_alignment(Alignment::Right) .title_style(self.palette.create_title_style(false)) .title("Blocks")); let scrollbar = Scrollbar::default() .orientation(ScrollbarOrientation::VerticalRight) .begin_symbol(None) .end_symbol(None) .style(self.palette.create_scrollbar_style()); frame.render_stateful_widget(table, place, &mut self.table_state); frame.render_stateful_widget( scrollbar, place.inner(Margin { vertical: 1, horizontal: 1 }), &mut self.scroll_state, ); Ok(()) } }