[WIP] New UI Logic

This commit is contained in:
Alex Emmet 2026-02-18 10:30:41 +01:00
commit 6db4e806f8
26 changed files with 815 additions and 674 deletions

View file

@ -1,10 +1,10 @@
use std::collections::VecDeque;
use crate::gui::elements::log_card::UiLogEntry;
use json::{JsonValue, object};
use std::collections::VecDeque;
#[derive(Clone)]
pub struct AppState {
pub logs: VecDeque<String>,
pub logs: VecDeque<UiLogEntry>,
pub cpu: Vec<(f64, f64)>,
pub ram: Vec<(f64, f64)>,
pub ping: Vec<(f64, f64)>,
@ -28,13 +28,17 @@ impl AppState {
}
}
pub fn push_log(&mut self, msg: String) {
pub fn push_log(&mut self, msg: UiLogEntry) {
if self.logs.len() >= MAX_LOGS {
self.logs.pop_front();
}
self.logs.push_back(msg);
}
pub fn get_logs(&self) -> &VecDeque<UiLogEntry> {
&self.logs
}
pub fn push_cpu(&mut self, pt: (f64, f64)) {
self.cpu.push(pt);
if self.cpu.len() > MAX_POINTS {
@ -112,28 +116,23 @@ impl AppState {
let len = data.len();
if len >= width_usize {
// Trim data to fit
data[len - width_usize..].to_vec()
} else {
let mut result = Vec::with_capacity(width_usize);
// Define X spacing (so dummy points are properly spaced across the canvas)
let dx = 1.0;
let pad_len = width_usize - len;
// If we have real data, use its first x position to determine where to start padding
let start_x = data
.first()
.map(|(x, _)| x - (dx * pad_len as f64))
.unwrap_or(0.0);
let _ = data.first().map(|(_, y)| *y).unwrap_or(0.0);
// Fill padding with increasing x positions so they're visible
for i in 0..pad_len {
result.push((start_x + i as f64 * dx, -1 as f64));
}
// Then append the real data
result.extend_from_slice(data);
result
}

View file

@ -0,0 +1,35 @@
use std::any::Any;
use crossterm::event::KeyEvent;
use ratatui::{Frame, layout::Rect};
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen};
pub trait Element: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn render(&self, f: &mut Frame, r: Rect);
}
pub trait InfoElement: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn as_element(&self) -> &dyn Element;
fn as_element_mut(&mut self) -> &mut dyn Element;
fn get_info_screen(&self) -> Box<dyn Screen>;
}
pub trait InteractableElement: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn as_element(&self) -> &dyn Element;
fn as_element_mut(&mut self) -> &mut dyn Element;
fn interact(&mut self, key: KeyEvent) -> InteractionResult;
fn can_focus(&self) -> bool;
fn is_focused(&self) -> bool;
fn focus(&mut self, f: bool);
}

View file

@ -0,0 +1,113 @@
use crate::gui::elements::elements::InteractableElement;
use crate::gui::interaction_result::InteractionResult;
use crate::{APP_STATE, gui::elements::elements::Element};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
};
use std::any::Any;
use std::sync::Arc;
#[derive(Clone)]
pub struct UiLogEntry {
pub line: String,
pub color: Color,
}
pub struct LogCard {
focused: bool,
scroll: u16,
}
impl LogCard {
pub fn new() -> Self {
Self {
focused: false,
scroll: 0,
}
}
}
impl Element for LogCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn render(&self, f: &mut Frame, area: Rect) {
let state = APP_STATE.lock().unwrap();
let logs = state.get_logs();
let lines: Vec<Line> = logs
.iter()
.map(|log| Line::from(Span::raw(log.line.clone())).style(log.color))
.collect();
let block = Block::default()
.title("Logs")
.borders(Borders::ALL)
.border_style(if self.focused {
Style::default().fg(Color::Yellow)
} else {
Style::default()
});
let paragraph = Paragraph::new(lines)
.block(block)
.wrap(Wrap { trim: false })
.scroll((self.scroll, 0));
f.render_widget(paragraph, area);
}
}
impl InteractableElement for LogCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn as_element(&self) -> &dyn Element {
self
}
fn as_element_mut(&mut self) -> &mut dyn Element {
self
}
fn interact(&mut self, key: KeyEvent) -> InteractionResult {
match key.code {
KeyCode::Up => {
if self.scroll > 0 {
self.scroll -= 1;
}
InteractionResult::Handeled
}
KeyCode::Down => {
self.scroll += 1;
InteractionResult::Handeled
}
_ => InteractionResult::Unhandeled,
}
}
fn can_focus(&self) -> bool {
true
}
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self, f: bool) {
self.focused = f;
}
}

View file

@ -1,112 +0,0 @@
use std::time::Duration;
use crate::ACTIVE_TASKS;
use crate::{RELOAD, SHUTDOWN, gui::tui::UNIQUE, util::config_util::CONFIG};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, poll, read};
use json::JsonValue;
pub fn setup_input_handler() {
tokio::spawn(async move {
{
let mut tasks = ACTIVE_TASKS.lock().unwrap();
tasks.push("Input Handler".to_string());
}
loop {
{
let should_shutdown = *SHUTDOWN.read().await;
if should_shutdown {
break;
}
}
let has_event = match poll(Duration::from_millis(100)) {
Ok(true) => true,
Ok(false) => false,
Err(_) => false,
};
if has_event {
match read() {
Ok(event) => {
if let Event::Key(key_event) = event {
handle_input(key_event).await;
}
}
Err(_) => (),
}
}
}
{
let mut tasks = ACTIVE_TASKS.lock().unwrap();
tasks.retain(|t| t != "Input Handler");
}
});
}
pub async fn handle_input(key: KeyEvent) {
match (key.code, key.modifiers) {
(KeyCode::Char('q'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true;
}
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true;
}
(KeyCode::Char('r'), KeyModifiers::CONTROL) => {
{
*RELOAD.write().await = true;
}
{
*SHUTDOWN.write().await = true;
}
}
(KeyCode::Backspace, KeyModifiers::NONE) => {
let password: String = {
let cfg = CONFIG.read().await;
cfg.get("password").as_str().unwrap_or("").to_string()
};
let password: &str = &password;
let password = match password.char_indices().next_back() {
Some((i, _)) => &password[..i],
_ => password,
};
CONFIG
.write()
.await
.change("password", JsonValue::String(password.to_string()));
CONFIG.write().await.update();
*UNIQUE.write().await = true;
}
(KeyCode::Char(c), KeyModifiers::NONE) => {
let password: String = {
let cfg = CONFIG.read().await;
cfg.get("password").as_str().unwrap_or("").to_string()
};
let password = &format!("{}{}", password, c);
CONFIG
.write()
.await
.change("password", JsonValue::String(password.to_string()));
CONFIG.write().await.update();
*UNIQUE.write().await = true;
}
(KeyCode::Char(c), KeyModifiers::SHIFT) => {
let password: String = {
let cfg = CONFIG.read().await;
cfg.get("password").as_str().unwrap_or("").to_string()
};
let password = &format!("{}{}", password, c);
CONFIG
.write()
.await
.change("password", JsonValue::String(password.to_string()));
CONFIG.write().await.update();
*UNIQUE.write().await = true;
}
_ => {}
}
}

View file

@ -0,0 +1,27 @@
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use crate::gui::screens::screens::Screen;
pub enum InteractionResult {
OpenScreen {
screen: Box<dyn Screen>,
},
OpenFutureScreen {
screen: Pin<Box<dyn Future<Output = Box<dyn Screen>> + Send>>,
},
Handeled,
Unhandeled,
}
impl Debug for InteractionResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"),
InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"),
InteractionResult::Handeled => write!(f, "Handeled"),
InteractionResult::Unhandeled => write!(f, "Unhandeled"),
}
}
}

View file

@ -1,108 +0,0 @@
use crate::ACTIVE_TASKS;
use crate::APP_STATE;
use crate::SHUTDOWN;
use crate::gui::tui::UNIQUE;
use crate::langu::language_manager::format;
use crate::langu::language_manager::from_key;
use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes};
use std::{thread, time::Duration};
use sysinfo::{RefreshKind, System};
pub fn log_cv(cv: &CommunicationValue) {
if cv.is_type(CommunicationType::identification_response) {
let args = [cv.get_data(DataTypes::accepted).unwrap().as_str().unwrap()];
log_message(format(&"identification_response", &args));
} else {
log_message_trans(format!("{:?}", &cv.get_type()));
}
tokio::spawn(async move {
*UNIQUE.write().await = true;
});
}
pub fn log_message_trans(key: impl Into<String>) {
APP_STATE.lock().unwrap().push_log(from_key(&key.into()));
tokio::spawn(async move {
*UNIQUE.write().await = true;
});
}
pub fn log_message(msg: impl Into<String>) {
APP_STATE.lock().unwrap().push_log(msg.into());
tokio::spawn(async move {
*UNIQUE.write().await = true;
});
}
pub fn log_message_format(msg: impl Into<String>, args: &[&str]) {
APP_STATE
.lock()
.unwrap()
.push_log(format(&msg.into(), args));
tokio::spawn(async move {
*UNIQUE.write().await = true;
});
}
pub fn setup() {
tokio::spawn(async move {
{
ACTIVE_TASKS.lock().unwrap().push("metrics".to_string());
}
let mut sys = System::new_with_specifics(RefreshKind::new());
let mut last_total_received = 0u64;
let mut last_total_transmitted = 0u64;
let mut counter = 0.0;
loop {
if *SHUTDOWN.read().await {
break;
}
sys.refresh_all();
let mut tcpu = 0;
for cpu in sys.cpus() {
tcpu += cpu.cpu_usage() as i64;
tcpu /= 2;
}
let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0;
let total_received = 0u64;
let total_transmitted = 0u64;
let delta_received = if last_total_received == 0 {
0
} else {
total_received.saturating_sub(last_total_received)
};
let delta_transmitted = if last_total_transmitted == 0 {
0
} else {
total_transmitted.saturating_sub(last_total_transmitted)
};
last_total_received = total_received;
last_total_transmitted = total_transmitted;
let net_down = delta_received as f64;
let net_up = delta_transmitted as f64;
{
let mut st = APP_STATE.lock().unwrap();
st.push_cpu((counter, tcpu as f64));
st.push_ram((counter, ram));
st.push_net_down((counter, net_down));
st.push_net_up((counter, net_up));
st.sys_info = format!("NetDown: {} NetUp: {}", delta_received, delta_transmitted);
}
counter += 1.0;
*UNIQUE.write().await = true;
thread::sleep(Duration::from_millis(1000));
}
{
ACTIVE_TASKS
.lock()
.unwrap()
.retain(|t| !t.eq(&"metrics".to_string()));
}
});
}

View file

@ -1,8 +1,10 @@
pub mod app_state;
pub mod log_panel;
pub mod settings_panel;
pub mod widgets {
pub mod betterblock;
pub mod elements {
pub mod elements;
pub mod log_card;
}
pub mod input_handler;
pub mod tui;
pub mod screens {
pub mod screens;
}
pub mod app_state;
pub mod interaction_result;
pub mod ui;

View file

@ -0,0 +1,27 @@
use std::{any::Any, sync::Arc};
use crossterm::event::KeyEvent;
use ratatui::{Frame, layout::Rect};
use crate::gui::{interaction_result::InteractionResult, ui::UI};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavDirection {
Up,
Down,
Left,
Right,
Next,
Prev,
}
pub trait Screen: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn get_ui(&self) -> &Arc<UI>;
fn render(&self, f: &mut Frame, rect: Rect);
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult;
}

View file

@ -1,43 +0,0 @@
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Margin, Rect},
text::Span,
widgets::{Block, Paragraph},
};
use crate::gui::app_state::AppState;
pub fn draw(frame: &mut Frame, area: Rect, block: Block, password: String, _state: AppState) {
frame.render_widget(block, area);
let padded = area.inner(Margin {
horizontal: 1,
vertical: 1,
});
let info = vec![
"This is the technical end of your Iota",
"Press Ctrl+Q to quit",
"Press Ctrl+R to reload",
"Select a password for the WebUI",
];
let mut constraints: Vec<Constraint> = Vec::new();
for _ in 0..info.len() {
constraints.push(Constraint::Length(1));
}
constraints.push(Constraint::Length(3));
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(padded);
for i in 0..info.len() {
frame.render_widget(Paragraph::new(info[i]), chunks[i]);
}
let prefix = Span::raw(format!("select password : {}", password));
frame.render_widget(Paragraph::new(prefix), chunks[info.len()]);
}

View file

@ -1,230 +0,0 @@
use std::{io::Stdout, sync::Arc, time::Duration};
use crate::{
APP_STATE, SHUTDOWN,
gui::{settings_panel, widgets::betterblock::draw_block_joins},
util::config_util::CONFIG,
};
use once_cell::sync::Lazy;
use ratatui::{
Frame, Terminal,
layout::{Constraint, Direction, Layout, Rect},
prelude::CrosstermBackend,
style::Color,
widgets::{
Block, Borders, List, ListItem,
canvas::{Canvas, Line},
},
};
use tokio::{
self,
sync::{Mutex, RwLock},
};
pub static UNIQUE: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true));
pub static TERMINAL: Lazy<Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>> =
Lazy::new(|| Arc::new(Mutex::new(ratatui::init())));
pub fn start_tui() {
tokio::spawn(async move {
loop {
if *SHUTDOWN.read().await {
break;
}
if *UNIQUE.read().await {
render_tui().await;
} else {
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
ratatui::restore();
});
}
pub async fn render_tui() {
let password = CONFIG
.read()
.await
.get("password")
.as_str()
.unwrap_or("")
.to_string();
let mut terminal = TERMINAL.lock().await;
let _ = terminal.draw(|f| {
let area = f.area();
let is_horizontal = area.width >= 120;
let chunks = if is_horizontal {
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(40),
Constraint::Percentage(60),
Constraint::Min(40),
])
.split(area)
} else {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
.split(area)
};
let state = APP_STATE.lock().unwrap().clone();
// LOGS PANEL
{
let borders = if is_horizontal {
Borders::LEFT.union(Borders::TOP).union(Borders::BOTTOM)
} else {
Borders::LEFT.union(Borders::RIGHT).union(Borders::TOP)
};
let items: Vec<ListItem> = state
.logs
.iter()
.rev()
.map(|s| ListItem::new(s.clone()))
.collect();
let list = List::new(items).block(Block::default().title("Logs").borders(borders));
f.render_widget(list, chunks[0]);
}
// SETTINGS PANEL
{
let borders = if is_horizontal {
Borders::LEFT.union(Borders::TOP).union(Borders::BOTTOM)
} else {
Borders::ALL
};
settings_panel::draw(
f,
chunks[1],
Block::default().title("Settings").borders(borders),
password,
state.clone(),
);
if is_horizontal {
draw_block_joins(
f,
chunks[1],
Borders::TOP.union(Borders::LEFT).union(Borders::BOTTOM),
Borders::LEFT,
);
} else {
draw_block_joins(f, chunks[1], Borders::ALL, Borders::TOP);
}
}
// PERFORMANCE GRAPHS
if is_horizontal {
let stack = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
])
.split(chunks[2]);
render_graphs(
f,
stack[0],
"CPU".into(),
"%".into(),
state.with_width(38).cpu,
Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT),
Color::Cyan,
);
draw_block_joins(
f,
stack[0],
Borders::TOP.union(Borders::LEFT),
Borders::LEFT,
);
render_graphs(
f,
stack[1],
"RAM".into(),
"%".into(),
state.with_width(38).ram,
Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT),
Color::Green,
);
draw_block_joins(
f,
stack[1],
Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT),
Borders::TOP,
);
render_graphs(
f,
stack[2],
"PING".into(),
"ms".into(),
state.with_width(38).ping,
Borders::ALL,
Color::Magenta,
);
draw_block_joins(f, stack[2], Borders::ALL, Borders::TOP);
draw_block_joins(
f,
stack[2],
Borders::BOTTOM.union(Borders::LEFT),
Borders::LEFT,
);
}
});
*UNIQUE.write().await = false;
}
pub fn render_graphs(
f: &mut Frame<'_>,
area: Rect,
title: String,
unit: String,
graph: Vec<(f64, f64)>,
borders: Borders,
color: Color,
) {
let min_x = graph.first().map(|(x, _)| *x).unwrap_or(0.0);
let max_x = graph.last().map(|(x, _)| *x).unwrap_or(100.0);
let min_y = graph
.iter()
.map(|(_, y)| *y)
.filter(|y| *y > 0.0)
.min_by(|a, b| a.total_cmp(b))
.unwrap_or(0.0);
let max_y = graph.iter().map(|(_, y)| *y).fold(f64::MIN, f64::max);
let block = Block::default()
.title(format!(
"─{}:─{}{}──{}/{}─MIN/MAX",
title,
graph.last().unwrap_or(&(0.0, 0.0)).1 as i64,
unit,
min_y as i64,
max_y as i64
))
.borders(borders);
let canvas = Canvas::default()
.block(block)
.x_bounds([min_x, max_x])
.y_bounds([0.0, 100.0])
.paint(|ctx| {
for (x, y) in &graph {
ctx.draw(&Line {
x1: *x,
y1: 0.0,
x2: *x,
y2: *y,
color,
});
}
});
f.render_widget(canvas, area);
}

197
src/gui/ui.rs Normal file
View file

@ -0,0 +1,197 @@
use crossterm::{
cursor::MoveTo,
execute,
terminal::{Clear, ClearType, EnterAlternateScreen},
};
use ratatui::{
Terminal,
backend::{Backend, CrosstermBackend},
layout::Size,
};
use std::{
io::{self, Error},
sync::{Arc, Mutex},
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen};
struct ResizableBackend<'a> {
backend: CrosstermBackend<&'a mut Vec<u8>>,
size: Size,
}
impl<'a> ResizableBackend<'a> {
fn new(buf: &'a mut Vec<u8>, width: u16, height: u16) -> Self {
Self {
backend: CrosstermBackend::new(buf),
size: Size::new(width, height),
}
}
}
impl<'a> Backend for ResizableBackend<'a> {
type Error = Error;
fn draw<'b, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'b ratatui::buffer::Cell)>,
{
self.backend.draw(content)
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()
}
fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()
}
#[allow(deprecated)]
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
#[allow(deprecated)]
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)
}
fn clear(&mut self) -> io::Result<()> {
self.backend.clear()
}
fn clear_region(&mut self, region: ratatui::backend::ClearType) -> io::Result<()> {
self.backend.clear_region(region)
}
fn size(&self) -> Result<Size, Error> {
Ok(self.size)
}
fn flush(&mut self) -> io::Result<()> {
self.backend.flush()
}
fn get_cursor_position(&mut self) -> Result<ratatui::prelude::Position, Self::Error> {
todo!()
}
fn set_cursor_position<P: Into<ratatui::prelude::Position>>(
&mut self,
_position: P,
) -> Result<(), Self::Error> {
todo!()
}
fn window_size(&mut self) -> Result<ratatui::prelude::backend::WindowSize, Self::Error> {
todo!()
}
}
/// UI state and rendering
pub struct UI {
pub cols: Arc<Mutex<u16>>,
pub rows: Arc<Mutex<u16>>,
screen: Arc<Mutex<Option<Box<dyn Screen>>>>,
cached_render: Arc<Mutex<Vec<u8>>>,
}
impl UI {
pub fn new(cols: u16, rows: u16) -> Self {
Self {
cols: Arc::new(Mutex::new(cols)),
rows: Arc::new(Mutex::new(rows)),
screen: Arc::new(Mutex::new(None)),
cached_render: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn set_screen(&self, screen: Box<dyn Screen>) {
*self.screen.lock().unwrap() = Some(screen);
}
pub fn resize(&self, cols: u32, rows: u32) {
*self.cols.lock().unwrap() = cols as u16;
*self.rows.lock().unwrap() = rows as u16;
}
pub async fn handle_input(&self, input: &[u8]) {
let key_event = if input.len() == 1 {
let c = input[0] as char;
if c.is_ascii() {
Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))
} else {
None
}
} else {
None
};
let event = match input {
b"\x1b[A" => Some(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)),
b"\x1b[B" => Some(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)),
b"\x1b[C" => Some(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)),
b"\x1b[D" => Some(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)),
b"\r" | b"\n" => Some(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
b"\x7f" => Some(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)),
_ => key_event,
};
if let Some(event) = event {
let result = {
let mut guard = self.screen.lock().unwrap();
if let Some(screen) = guard.as_mut() {
screen.handle_input(event)
} else {
return;
}
};
match result {
InteractionResult::OpenScreen { screen } => {
self.set_screen(screen);
}
InteractionResult::OpenFutureScreen { screen: fut } => {
let ui = self.clone();
let screen = fut.await;
ui.set_screen(screen);
}
_ => {}
}
}
}
pub fn render(&self) -> Vec<u8> {
let mut buf = Vec::new();
execute!(
&mut buf,
EnterAlternateScreen,
Clear(ClearType::All),
MoveTo(0, 0)
)
.unwrap();
{
let backend = ResizableBackend::new(
&mut buf,
*self.cols.lock().unwrap(),
*self.rows.lock().unwrap(),
);
let mut terminal = Terminal::new(backend).unwrap();
if let Some(screen) = self.screen.lock().unwrap().as_ref() {
let _ = terminal.draw(|f| screen.render(f, f.area()));
}
}
*self.cached_render.lock().unwrap() = buf.clone();
buf
}
}

View file

@ -1,68 +0,0 @@
use ratatui::layout::Rect;
use ratatui::prelude::*;
use ratatui::style::Style;
use ratatui::widgets::Borders;
fn set_join_char(frame: &mut Frame, x: u16, y: u16, c: char) {
frame
.buffer_mut()
.set_string(x, y, c.to_string(), Style::default());
}
/// Draws corner join characters only if the block has borders on that side
pub fn draw_block_joins(frame: &mut Frame, area: Rect, borders: Borders, joins: Borders) {
let x0 = area.x;
let y0 = area.y;
let x1 = area.x + area.width - 1;
let y1 = area.y + area.height - 1;
// Top-left corner
if borders.contains(Borders::TOP) && borders.contains(Borders::LEFT) {
let top_left = match (joins.contains(Borders::TOP), joins.contains(Borders::LEFT)) {
(true, true) => '┼',
(true, false) => '├',
(false, true) => '┬',
(false, false) => '┌',
};
set_join_char(frame, x0, y0, top_left);
}
// Top-right corner
if borders.contains(Borders::TOP) && borders.contains(Borders::RIGHT) {
let top_right = match (joins.contains(Borders::TOP), joins.contains(Borders::RIGHT)) {
(true, true) => '┼',
(true, false) => '┤',
(false, true) => '┬',
(false, false) => '┐',
};
set_join_char(frame, x1, y0, top_right);
}
// Bottom-left corner
if borders.contains(Borders::BOTTOM) && borders.contains(Borders::LEFT) {
let bottom_left = match (
joins.contains(Borders::BOTTOM),
joins.contains(Borders::LEFT),
) {
(true, true) => '┼',
(true, false) => '├',
(false, true) => '┴',
(false, false) => '└',
};
set_join_char(frame, x0, y1, bottom_left);
}
// Bottom-right corner
if borders.contains(Borders::BOTTOM) && borders.contains(Borders::RIGHT) {
let bottom_right = match (
joins.contains(Borders::BOTTOM),
joins.contains(Borders::RIGHT),
) {
(true, true) => '┼',
(true, false) => '┤',
(false, true) => '┴',
(false, false) => '┘',
};
set_join_char(frame, x1, y1, bottom_right);
}
}