better ui with 3 lines now
This commit is contained in:
@@ -9,7 +9,7 @@ name = "projekt_final"
|
||||
path = "./src/bin/main.rs"
|
||||
|
||||
[dependencies]
|
||||
pages-tui = { path = "../../../pages-tui", default-features = false }
|
||||
pages-tui = { path = "../../../pages-tui" }
|
||||
esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32"] }
|
||||
esp-hal = { version = "=1.0.0-rc.0", features = [
|
||||
"esp32",
|
||||
|
||||
@@ -10,11 +10,8 @@ use pages_tui::input::Key;
|
||||
/// IMU sensor reading from MPU6050
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
pub struct ImuReading {
|
||||
/// Acceleration in g (earth gravity units)
|
||||
pub accel_g: [f32; 3],
|
||||
/// Angular velocity in degrees per second
|
||||
pub gyro_dps: [f32; 3],
|
||||
/// Temperature in Celsius
|
||||
pub temp_c: f32,
|
||||
/// Timestamp in milliseconds since boot
|
||||
pub timestamp_ms: u64,
|
||||
@@ -35,4 +32,5 @@ pub enum DisplayCommand {
|
||||
Clear,
|
||||
|
||||
PushKey(Key),
|
||||
AddChatMessage(HString<24>),
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// src/display/api.rs
|
||||
//! Public API for the display feature.
|
||||
//!
|
||||
//! Other parts of the system use this module to send commands to the display.
|
||||
//! The actual rendering happens in `task.rs`.
|
||||
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_sync::channel::{Channel, Receiver, TrySendError};
|
||||
@@ -11,72 +8,47 @@ use heapless::String;
|
||||
use crate::contracts::{DisplayCommand, ImuReading};
|
||||
use pages_tui::input::Key;
|
||||
|
||||
/// Queue size for display commands.
|
||||
/// Moderate size to handle bursts without dropping.
|
||||
const QUEUE_SIZE: usize = 8;
|
||||
|
||||
/// Channel for sending commands to the display task.
|
||||
pub(crate) static DISPLAY_CHANNEL: Channel<CriticalSectionRawMutex, DisplayCommand, QUEUE_SIZE> =
|
||||
Channel::new();
|
||||
|
||||
/// Send a command to the display.
|
||||
///
|
||||
/// This is async and will wait if the queue is full.
|
||||
/// For fire-and-forget, use `try_send`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// display::api::send(DisplayCommand::SetStatus("Hello".try_into().unwrap())).await;
|
||||
/// ```
|
||||
pub async fn send(cmd: DisplayCommand) {
|
||||
DISPLAY_CHANNEL.send(cmd).await;
|
||||
}
|
||||
|
||||
/// Try to send a command without waiting.
|
||||
///
|
||||
/// Returns `Err(cmd)` if the queue is full.
|
||||
pub fn try_send(cmd: DisplayCommand) -> Result<(), DisplayCommand> {
|
||||
DISPLAY_CHANNEL.try_send(cmd).map_err(|e| match e {
|
||||
TrySendError::Full(command) => command,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a receiver for display commands (internal use).
|
||||
///
|
||||
/// Used by the display task to receive commands.
|
||||
pub(crate) fn receiver() -> Receiver<'static, CriticalSectionRawMutex, DisplayCommand, QUEUE_SIZE> {
|
||||
DISPLAY_CHANNEL.receiver()
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Convenience functions for common commands
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Send IMU data to the display.
|
||||
pub async fn show_imu(reading: ImuReading) {
|
||||
send(DisplayCommand::SetImu(reading)).await;
|
||||
}
|
||||
|
||||
/// Set the status line.
|
||||
pub async fn set_status(text: &str) {
|
||||
let mut s = String::<32>::new();
|
||||
let _ = s.push_str(&text[..text.len().min(32)]);
|
||||
send(DisplayCommand::SetStatus(s)).await;
|
||||
}
|
||||
|
||||
/// Show an error message.
|
||||
pub async fn show_error(text: &str) {
|
||||
let mut s = String::<64>::new();
|
||||
let _ = s.push_str(&text[..text.len().min(64)]);
|
||||
send(DisplayCommand::ShowError(s)).await;
|
||||
}
|
||||
|
||||
/// Update MQTT status indicator.
|
||||
pub async fn set_mqtt_status(connected: bool, msg_count: u32) {
|
||||
send(DisplayCommand::SetMqttStatus { connected, msg_count }).await;
|
||||
}
|
||||
|
||||
/// Clear the display.
|
||||
pub async fn clear() {
|
||||
send(DisplayCommand::Clear).await;
|
||||
}
|
||||
@@ -84,3 +56,10 @@ pub async fn clear() {
|
||||
pub async fn push_key(key: Key) {
|
||||
send(DisplayCommand::PushKey(key)).await;
|
||||
}
|
||||
|
||||
/// Add a chat message (for Chat page)
|
||||
pub async fn add_chat_message(msg: &str) {
|
||||
let mut s = String::<24>::new();
|
||||
let _ = s.push_str(&msg[..msg.len().min(24)]);
|
||||
send(DisplayCommand::AddChatMessage(s)).await;
|
||||
}
|
||||
|
||||
@@ -25,17 +25,13 @@ pub async fn display_task(i2c: I2cDevice) {
|
||||
|
||||
if let Err(e) = display.init() {
|
||||
error!("Display init failed: {:?}", e);
|
||||
loop {
|
||||
Timer::after(Duration::from_secs(60)).await;
|
||||
}
|
||||
loop { Timer::after(Duration::from_secs(60)).await; }
|
||||
}
|
||||
|
||||
let config = EmbeddedBackendConfig {
|
||||
flush_callback: Box::new(
|
||||
|d: &mut Ssd1306<_, _, BufferedGraphicsMode<DisplaySize128x32>>| {
|
||||
flush_callback: Box::new(|d: &mut Ssd1306<_, _, BufferedGraphicsMode<DisplaySize128x32>>| {
|
||||
let _ = d.flush();
|
||||
},
|
||||
),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -46,47 +42,43 @@ pub async fn display_task(i2c: I2cDevice) {
|
||||
let mut orchestrator = Orchestrator::<Screen>::new();
|
||||
let rx = receiver();
|
||||
|
||||
// Register all pages
|
||||
// Register pages
|
||||
orchestrator.register_page("menu".into(), Screen::Menu);
|
||||
orchestrator.register_page("imu".into(), Screen::Imu);
|
||||
orchestrator.register_page("mqtt".into(), Screen::Mqtt);
|
||||
orchestrator.register_page("status".into(), Screen::Status);
|
||||
orchestrator.register_page("chat".into(), Screen::Chat);
|
||||
|
||||
// Key bindings:
|
||||
// - Tab (Next button): cycle focus between Prev/Next buttons
|
||||
// - Enter (Select button): trigger the focused button's action
|
||||
orchestrator.bind(Key::tab(), ComponentAction::Next);
|
||||
orchestrator.bind(Key::enter(), ComponentAction::Select);
|
||||
|
||||
// Navigate to initial page
|
||||
let _ = orchestrator.navigate_to("imu".into());
|
||||
let _ = orchestrator.navigate_to("menu".into());
|
||||
|
||||
info!("Display ready");
|
||||
|
||||
loop {
|
||||
while let Ok(cmd) = rx.try_receive() {
|
||||
match cmd {
|
||||
DisplayCommand::PushKey(key) => {
|
||||
if key == Key::tab() {
|
||||
// Cycle focus between buttons
|
||||
orchestrator.focus_manager_mut().wrap_next();
|
||||
info!("Focus changed to: {:?}", orchestrator.focus_manager().current());
|
||||
} else if key == Key::enter() {
|
||||
// Process Select - this triggers Component::handle()
|
||||
let events = orchestrator.process_frame(key);
|
||||
|
||||
// Handle navigation events
|
||||
if let Ok(events) = events {
|
||||
if let Ok(events) = orchestrator.process_frame(key) {
|
||||
for event in events {
|
||||
match event {
|
||||
ScreenEvent::GoToImu => {
|
||||
let _ = orchestrator.navigate_to("imu".into());
|
||||
}
|
||||
ScreenEvent::GoToChat => {
|
||||
let _ = orchestrator.navigate_to("chat".into());
|
||||
}
|
||||
ScreenEvent::NavigatePrev => {
|
||||
if let Some(current) = orchestrator.current_id() {
|
||||
let prev = prev_page_id(current.as_str());
|
||||
info!("Navigating to prev: {}", prev);
|
||||
if let Some(cur) = orchestrator.current_id() {
|
||||
let prev = prev_page_id(cur.as_str());
|
||||
let _ = orchestrator.navigate_to(prev.into());
|
||||
}
|
||||
}
|
||||
ScreenEvent::NavigateNext => {
|
||||
if let Some(current) = orchestrator.current_id() {
|
||||
let next = next_page_id(current.as_str());
|
||||
info!("Navigating to next: {}", next);
|
||||
if let Some(cur) = orchestrator.current_id() {
|
||||
let next = next_page_id(cur.as_str());
|
||||
let _ = orchestrator.navigate_to(next.into());
|
||||
}
|
||||
}
|
||||
@@ -99,10 +91,8 @@ pub async fn display_task(i2c: I2cDevice) {
|
||||
}
|
||||
}
|
||||
|
||||
// Render current state
|
||||
if let Some(current_screen) = orchestrator.current() {
|
||||
let focused = orchestrator.focus_manager().current();
|
||||
render_frame(&mut terminal, current_screen, focused, &state);
|
||||
if let Some(screen) = orchestrator.current() {
|
||||
render_frame(&mut terminal, screen, orchestrator.focus_manager().current(), &state);
|
||||
}
|
||||
|
||||
Timer::after(Duration::from_millis(REFRESH_INTERVAL_MS)).await;
|
||||
|
||||
@@ -4,32 +4,36 @@ use alloc::format;
|
||||
use heapless::String as HString;
|
||||
use pages_tui::prelude::*;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
layout::Rect,
|
||||
style::Stylize,
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
};
|
||||
use log::info;
|
||||
|
||||
/// Focusable buttons available on every page
|
||||
/// Focus targets - different per screen
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum PageButton {
|
||||
Prev,
|
||||
Next,
|
||||
pub enum PageFocus {
|
||||
MenuImu,
|
||||
MenuChat,
|
||||
NavPrev,
|
||||
NavNext,
|
||||
}
|
||||
|
||||
impl FocusId for PageButton {}
|
||||
impl FocusId for PageFocus {}
|
||||
|
||||
/// All available buttons for iteration
|
||||
const PAGE_BUTTONS: &[PageButton] = &[PageButton::Prev, PageButton::Next];
|
||||
const MENU_TARGETS: &[PageFocus] = &[PageFocus::MenuImu, PageFocus::MenuChat];
|
||||
const NAV_TARGETS: &[PageFocus] = &[PageFocus::NavPrev, PageFocus::NavNext];
|
||||
|
||||
/// Display state shared between pages
|
||||
/// Display state
|
||||
pub struct DisplayState {
|
||||
pub(crate) status: HString<32>,
|
||||
pub(crate) last_imu: Option<ImuReading>,
|
||||
pub(crate) last_error: Option<HString<64>>,
|
||||
pub(crate) mqtt_connected: bool,
|
||||
pub(crate) mqtt_msg_count: u32,
|
||||
pub(crate) chat_msg1: HString<24>,
|
||||
pub(crate) chat_msg2: HString<24>,
|
||||
}
|
||||
|
||||
impl Default for DisplayState {
|
||||
@@ -40,6 +44,8 @@ impl Default for DisplayState {
|
||||
last_error: None,
|
||||
mqtt_connected: false,
|
||||
mqtt_msg_count: 0,
|
||||
chat_msg1: HString::new(),
|
||||
chat_msg2: HString::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,20 +69,30 @@ impl DisplayState {
|
||||
self.status = HString::new();
|
||||
}
|
||||
DisplayCommand::PushKey(_) => {}
|
||||
DisplayCommand::AddChatMessage(msg) => {
|
||||
self.add_chat_message(msg.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_chat_message(&mut self, msg: &str) {
|
||||
self.chat_msg2 = self.chat_msg1.clone();
|
||||
self.chat_msg1.clear();
|
||||
let _ = self.chat_msg1.push_str(&msg[..msg.len().min(24)]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Screen {
|
||||
Menu,
|
||||
Imu,
|
||||
Mqtt,
|
||||
Status,
|
||||
Chat,
|
||||
}
|
||||
|
||||
/// Events emitted by screens for navigation
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ScreenEvent {
|
||||
GoToImu,
|
||||
GoToChat,
|
||||
NavigatePrev,
|
||||
NavigateNext,
|
||||
}
|
||||
@@ -84,46 +100,45 @@ pub enum ScreenEvent {
|
||||
impl Component for Screen {
|
||||
type Action = ComponentAction;
|
||||
type Event = ScreenEvent;
|
||||
type Focus = PageButton;
|
||||
type Focus = PageFocus;
|
||||
|
||||
fn handle(
|
||||
&mut self,
|
||||
focus: &Self::Focus,
|
||||
action: Self::Action,
|
||||
) -> Result<Option<Self::Event>, ComponentError> {
|
||||
match action {
|
||||
ComponentAction::Select => {
|
||||
info!("Select on {:?} screen, focused: {:?}", self, focus);
|
||||
// When Select is pressed, trigger navigation based on focused button
|
||||
match focus {
|
||||
PageButton::Prev => return Ok(Some(ScreenEvent::NavigatePrev)),
|
||||
PageButton::Next => return Ok(Some(ScreenEvent::NavigateNext)),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
if let ComponentAction::Select = action {
|
||||
info!("Select {:?} focus:{:?}", self, focus);
|
||||
return Ok(Some(match focus {
|
||||
PageFocus::MenuImu => ScreenEvent::GoToImu,
|
||||
PageFocus::MenuChat => ScreenEvent::GoToChat,
|
||||
PageFocus::NavPrev => ScreenEvent::NavigatePrev,
|
||||
PageFocus::NavNext => ScreenEvent::NavigateNext,
|
||||
}));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn targets(&self) -> &[Self::Focus] {
|
||||
// Every page has Prev and Next buttons
|
||||
PAGE_BUTTONS
|
||||
match self {
|
||||
Screen::Menu => MENU_TARGETS,
|
||||
Screen::Imu | Screen::Chat => NAV_TARGETS,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> Result<(), ComponentError> {
|
||||
info!("Entered screen: {:?}", self);
|
||||
info!("Enter: {:?}", self);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_exit(&mut self) -> Result<(), ComponentError> {
|
||||
info!("Exited screen: {:?}", self);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ============ PAGE ORDER ============
|
||||
|
||||
const PAGE_ORDER: &[&str] = &["imu", "mqtt", "status"];
|
||||
const PAGE_ORDER: &[&str] = &["menu", "imu", "chat"];
|
||||
|
||||
pub fn next_page_id(current: &str) -> &'static str {
|
||||
let idx = PAGE_ORDER.iter().position(|&p| p == current).unwrap_or(0);
|
||||
@@ -132,102 +147,120 @@ pub fn next_page_id(current: &str) -> &'static str {
|
||||
|
||||
pub fn prev_page_id(current: &str) -> &'static str {
|
||||
let idx = PAGE_ORDER.iter().position(|&p| p == current).unwrap_or(0);
|
||||
if idx == 0 {
|
||||
PAGE_ORDER[PAGE_ORDER.len() - 1]
|
||||
} else {
|
||||
PAGE_ORDER[idx - 1]
|
||||
}
|
||||
if idx == 0 { PAGE_ORDER[PAGE_ORDER.len() - 1] } else { PAGE_ORDER[idx - 1] }
|
||||
}
|
||||
|
||||
// ============ RENDERING ============
|
||||
// ============ RENDERING - NO LAYOUT, DIRECT RECTS ============
|
||||
// 128x32 display = 21 chars x 4 rows (6x8 font)
|
||||
|
||||
fn render_imu_content(state: &DisplayState) -> alloc::string::String {
|
||||
if let Some(ref imu) = state.last_imu {
|
||||
format!(
|
||||
"G:{:.0} {:.0} {:.0}\nT:{:.1}C",
|
||||
imu.gyro_dps[0], imu.gyro_dps[1], imu.gyro_dps[2], imu.temp_c
|
||||
)
|
||||
} else {
|
||||
format!("Waiting IMU...")
|
||||
}
|
||||
}
|
||||
|
||||
fn render_mqtt_content(state: &DisplayState) -> alloc::string::String {
|
||||
format!(
|
||||
"MQTT:{}\nMsg:{}",
|
||||
if state.mqtt_connected { "ON" } else { "OFF" },
|
||||
state.mqtt_msg_count
|
||||
)
|
||||
}
|
||||
|
||||
fn render_status_content(state: &DisplayState) -> alloc::string::String {
|
||||
format!("Status:\n{}", state.status.as_str())
|
||||
}
|
||||
|
||||
fn render_button(label: &str, focused: bool) -> Paragraph<'static> {
|
||||
let text = if focused {
|
||||
format!("[>{}]", label)
|
||||
} else {
|
||||
format!(" {} ", label)
|
||||
};
|
||||
|
||||
if focused {
|
||||
Paragraph::new(text).white().on_blue()
|
||||
} else {
|
||||
Paragraph::new(text).gray()
|
||||
}
|
||||
/// Get row rect (0-3) for 128x32 display
|
||||
#[inline]
|
||||
fn row(area: Rect, n: u16) -> Rect {
|
||||
Rect::new(area.x, area.y + n, area.width, 1)
|
||||
}
|
||||
|
||||
pub fn render_frame<B: ratatui::backend::Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
current_screen: &Screen,
|
||||
focused_button: Option<&PageButton>,
|
||||
focused: Option<&PageFocus>,
|
||||
state: &DisplayState,
|
||||
) {
|
||||
let _ = terminal.draw(|f| {
|
||||
// Handle errors with priority
|
||||
let area = f.area();
|
||||
|
||||
if let Some(ref err) = state.last_error {
|
||||
let content = format!("ERR: {}", err.as_str());
|
||||
f.render_widget(Paragraph::new(content).red().bold(), f.area());
|
||||
f.render_widget(Paragraph::new(format!("ERR:{}", err.as_str())).red().bold(), area);
|
||||
return;
|
||||
}
|
||||
|
||||
// Layout: content area + button bar
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(1), // Content
|
||||
Constraint::Length(1), // Button bar
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
// Render page content
|
||||
let content = match current_screen {
|
||||
Screen::Imu => render_imu_content(state),
|
||||
Screen::Mqtt => render_mqtt_content(state),
|
||||
Screen::Status => render_status_content(state),
|
||||
};
|
||||
|
||||
let style = match current_screen {
|
||||
Screen::Imu => Paragraph::new(content).cyan(),
|
||||
Screen::Mqtt => Paragraph::new(content).green(),
|
||||
Screen::Status => Paragraph::new(content).yellow(),
|
||||
};
|
||||
f.render_widget(style, chunks[0]);
|
||||
|
||||
// Render button bar
|
||||
let btn_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
])
|
||||
.split(chunks[1]);
|
||||
|
||||
let prev_focused = focused_button == Some(&PageButton::Prev);
|
||||
let next_focused = focused_button == Some(&PageButton::Next);
|
||||
|
||||
f.render_widget(render_button("<", prev_focused), btn_chunks[0]);
|
||||
f.render_widget(render_button(">", next_focused), btn_chunks[1]);
|
||||
match current_screen {
|
||||
Screen::Menu => render_menu(f, area, focused, state),
|
||||
Screen::Imu => render_imu(f, area, focused, state),
|
||||
Screen::Chat => render_chat(f, area, focused, state),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn render_menu(f: &mut ratatui::Frame, area: Rect, focused: Option<&PageFocus>, state: &DisplayState) {
|
||||
// Row 0 Title
|
||||
let conn = if state.mqtt_connected { "*" } else { "x" };
|
||||
f.render_widget(
|
||||
Paragraph::new(format!("-- ESP32 Hub {} --", conn)).white().bold(),
|
||||
row(area, 0),
|
||||
);
|
||||
|
||||
// Row 1 Sensors
|
||||
let imu_sel = focused == Some(&PageFocus::MenuImu);
|
||||
f.render_widget(
|
||||
Paragraph::new(if imu_sel { " > Sensors" } else { " Sensors" })
|
||||
.style(if imu_sel { ratatui::style::Style::default().white().bold() } else { ratatui::style::Style::default().white() }),
|
||||
row(area, 1),
|
||||
);
|
||||
|
||||
// Row 2 Messages
|
||||
let chat_sel = focused == Some(&PageFocus::MenuChat);
|
||||
f.render_widget(
|
||||
Paragraph::new(if chat_sel { " > Messages" } else { " Messages" })
|
||||
.style(if chat_sel { ratatui::style::Style::default().white().bold() } else { ratatui::style::Style::default().white() }),
|
||||
row(area, 2),
|
||||
);
|
||||
}
|
||||
|
||||
fn render_imu(f: &mut ratatui::Frame, area: Rect, focused: Option<&PageFocus>, state: &DisplayState) {
|
||||
if let Some(ref imu) = state.last_imu {
|
||||
// Row 0 Gyro
|
||||
// f.render_widget(
|
||||
// Paragraph::new(format!("G:{:+5.0}{:+5.0}{:+5.0}", imu.gyro_dps[0], imu.gyro_dps[1], imu.gyro_dps[2])).cyan(),
|
||||
// row(area, 0),
|
||||
// );
|
||||
// Row 1 Accel
|
||||
f.render_widget(
|
||||
Paragraph::new(format!("A:{:+.1} {:+.1} {:+.1}", imu.accel_g[0], imu.accel_g[1], imu.accel_g[2])).green(),
|
||||
row(area, 1),
|
||||
);
|
||||
// Row 2 Temp
|
||||
f.render_widget(
|
||||
Paragraph::new(format!("T: {:.1}C", imu.temp_c)).yellow(),
|
||||
row(area, 2),
|
||||
);
|
||||
} else {
|
||||
f.render_widget(Paragraph::new("Waiting for MPU...").white(), row(area, 1));
|
||||
}
|
||||
|
||||
// Row 3 Nav bar
|
||||
render_nav_bar(f, row(area, 2), focused);
|
||||
}
|
||||
|
||||
fn render_chat(f: &mut ratatui::Frame, area: Rect, focused: Option<&PageFocus>, state: &DisplayState) {
|
||||
// Row 0 Message 1
|
||||
if state.chat_msg1.is_empty() {
|
||||
f.render_widget(Paragraph::new("(no messages)").white(), row(area, 0));
|
||||
} else {
|
||||
f.render_widget(Paragraph::new(format!(">{}", state.chat_msg1.as_str())).green(), row(area, 0));
|
||||
}
|
||||
|
||||
// Row 1 Message 2
|
||||
if !state.chat_msg2.is_empty() {
|
||||
f.render_widget(Paragraph::new(format!(">{}", state.chat_msg2.as_str())).white(), row(area, 1));
|
||||
}
|
||||
|
||||
// Row 3 Nav bar
|
||||
render_nav_bar(f, row(area, 3), focused);
|
||||
}
|
||||
|
||||
/// Nav bar: --[<]-------[>]--
|
||||
fn render_nav_bar(f: &mut ratatui::Frame, area: Rect, focused: Option<&PageFocus>) {
|
||||
let prev_sel = focused == Some(&PageFocus::NavPrev);
|
||||
let next_sel = focused == Some(&PageFocus::NavNext);
|
||||
|
||||
let prev = if prev_sel { "[<]" } else { " < " };
|
||||
let next = if next_sel { "[>]" } else { " > " };
|
||||
|
||||
// Build the line: --[<]-------[>]--
|
||||
let line = format!("--{}-------{}--", prev, next);
|
||||
|
||||
f.render_widget(
|
||||
Paragraph::new(line).style(ratatui::style::Style::default().white()),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user