ghost-eye/src/components/explorer/block_explorer.rs
Uncle Stretch 0f9c0aa1f6
more use of generic components
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-08-26 20:58:30 +03:00

277 lines
9.5 KiB
Rust

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<BlockInfo>,
block_headers: std::collections::HashMap<u32, H256>,
block_authors: std::collections::HashMap<H256, String>,
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::IndexType> {
self.table_state.selected()
}
fn items_length(&self) -> Self::IndexType {
self.blocks.len()
}
fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
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<Option<Action>> {
self.apply_next_row(new_index)
}
fn apply_first_row(&mut self) -> Result<Option<Action>> {
match self.items_length() > 0 {
true => self.apply_next_row(0),
false => Ok(None),
}
}
fn apply_last_row(&mut self) -> Result<Option<Action>> {
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<Option<Action>> {
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<Option<Action>> {
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<Option<Action>> {
let maybe_block_number = self.blocks.get(index).map(|info| info.block_number);
Ok(Some(Action::UsedExplorerBlock(maybe_block_number)))
}
}
impl PartialComponent<CurrentTab> 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<Option<Action>> {
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<Option<Action>> {
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::<Vec<_>>();
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(())
}
}