pages-tui working
This commit is contained in:
14
mqtt_display/Cargo.lock
generated
14
mqtt_display/Cargo.lock
generated
@@ -1073,9 +1073,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1edcd5a338e64688fbdcb7531a846cfd3476a54784dcb918a0844682bc7ada5"
|
||||
checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
|
||||
dependencies = [
|
||||
"hash32",
|
||||
"stable_deref_trait",
|
||||
@@ -1305,6 +1305,13 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pages-tui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"heapless 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
@@ -1452,9 +1459,10 @@ dependencies = [
|
||||
"esp-hal-embassy",
|
||||
"esp-println",
|
||||
"esp-wifi",
|
||||
"heapless 0.9.1",
|
||||
"heapless 0.9.2",
|
||||
"log",
|
||||
"mousefood",
|
||||
"pages-tui",
|
||||
"ratatui",
|
||||
"rust-mqtt",
|
||||
"smoltcp",
|
||||
|
||||
@@ -9,6 +9,7 @@ name = "projekt_final"
|
||||
path = "./src/bin/main.rs"
|
||||
|
||||
[dependencies]
|
||||
pages-tui = { path = "../../../pages-tui", default-features = false }
|
||||
esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32"] }
|
||||
esp-hal = { version = "=1.0.0-rc.0", features = [
|
||||
"esp32",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// TODO WARNING core 1 should be logic, core 0 wifi, its flipped now
|
||||
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_futures::select::{select3, Either3};
|
||||
use embassy_futures::select::{select, Either, select3, Either3};
|
||||
use embassy_net::{Runner, StackResources};
|
||||
use embassy_sync::signal::Signal;
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
@@ -20,7 +20,9 @@ use esp_alloc as _;
|
||||
use esp_backtrace as _;
|
||||
|
||||
use esp_hal::{
|
||||
gpio::InputConfig,
|
||||
clock::CpuClock,
|
||||
gpio::{Input, Pull},
|
||||
i2c::master::{Config as I2cConfig, I2c},
|
||||
rng::Rng,
|
||||
system::{CpuControl, Stack},
|
||||
@@ -31,6 +33,7 @@ use esp_wifi::{
|
||||
EspWifiController,
|
||||
};
|
||||
|
||||
use pages_tui::input::Key;
|
||||
use log::info;
|
||||
use rust_mqtt::packet::v5::publish_packet::QualityOfService;
|
||||
use static_cell::StaticCell;
|
||||
@@ -122,6 +125,11 @@ async fn main(spawner: Spawner) -> ! {
|
||||
NETWORK_READY.wait().await;
|
||||
info!("Network ready, starting core 0 tasks");
|
||||
|
||||
let config = InputConfig::default().with_pull(Pull::Down);
|
||||
let button_select = Input::new(peripherals.GPIO32, config);
|
||||
let button_next = Input::new(peripherals.GPIO35, config);
|
||||
spawner.spawn(button_detection_task(button_select, button_next)).unwrap();
|
||||
|
||||
// Core 0: display and MPU tasks
|
||||
spawner.spawn(display::task::display_task(display_i2c)).expect("spawn display_task");
|
||||
spawner.spawn(mpu::task::mpu_task(mpu_i2c)).expect("spawn mpu_task");
|
||||
@@ -248,3 +256,25 @@ async fn connection_task(mut controller: WifiController<'static>) {
|
||||
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
|
||||
runner.run().await
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn button_detection_task(mut select_btn: Input<'static>, mut next_btn: Input<'static>) {
|
||||
use embassy_futures::select::Either;
|
||||
loop {
|
||||
match select(
|
||||
select_btn.wait_for_rising_edge(),
|
||||
next_btn.wait_for_rising_edge(),
|
||||
).await {
|
||||
Either::First(_) => {
|
||||
info!("Detection: GPIO 32 (Select) triggered!");
|
||||
display::api::push_key(Key::enter()).await;
|
||||
}
|
||||
Either::Second(_) => {
|
||||
info!("Detection: GPIO 35 (Next) triggered!");
|
||||
display::api::push_key(Key::tab()).await
|
||||
}
|
||||
}
|
||||
// Debounce: prevent mechanical bouncing from double-triggering
|
||||
embassy_time::Timer::after(embassy_time::Duration::from_millis(200)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! Features depend on these types, not on each other.
|
||||
|
||||
use heapless::String as HString;
|
||||
use pages_tui::input::Key;
|
||||
|
||||
/// IMU sensor reading from MPU6050
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
@@ -32,4 +33,6 @@ pub enum DisplayCommand {
|
||||
SetMqttStatus { connected: bool, msg_count: u32 },
|
||||
/// Clear the display to default state
|
||||
Clear,
|
||||
|
||||
PushKey(Key),
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use embassy_sync::channel::{Channel, Receiver, TrySendError};
|
||||
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.
|
||||
@@ -79,3 +80,7 @@ pub async fn set_mqtt_status(connected: bool, msg_count: u32) {
|
||||
pub async fn clear() {
|
||||
send(DisplayCommand::Clear).await;
|
||||
}
|
||||
|
||||
pub async fn push_key(key: Key) {
|
||||
send(DisplayCommand::PushKey(key)).await;
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
pub mod api;
|
||||
pub mod task;
|
||||
pub mod tui;
|
||||
|
||||
@@ -1,84 +1,24 @@
|
||||
// src/display/task.rs
|
||||
//! SSD1306 display rendering task optimized for 0.91" 128x32 OLED.
|
||||
|
||||
use embassy_time::{Duration, Timer};
|
||||
use log::{error, info};
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::format;
|
||||
use heapless::String;
|
||||
use mousefood::{EmbeddedBackend, EmbeddedBackendConfig};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Style, Stylize},
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
};
|
||||
use ssd1306::{
|
||||
mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface, Ssd1306,
|
||||
};
|
||||
use ratatui::Terminal;
|
||||
use ssd1306::{mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface, Ssd1306};
|
||||
|
||||
use crate::bus::I2cDevice;
|
||||
use crate::contracts::{DisplayCommand, ImuReading};
|
||||
use crate::display::api::receiver;
|
||||
use crate::display::tui::{render_frame, next_page_id, prev_page_id, DisplayState, Screen, ScreenEvent};
|
||||
use crate::contracts::DisplayCommand;
|
||||
use pages_tui::prelude::*;
|
||||
|
||||
/// Display refresh interval in milliseconds.
|
||||
const REFRESH_INTERVAL_MS: u64 = 100;
|
||||
|
||||
/// Internal state for what to render.
|
||||
struct DisplayState {
|
||||
status: String<32>,
|
||||
last_imu: Option<ImuReading>,
|
||||
last_error: Option<String<64>>,
|
||||
mqtt_connected: bool,
|
||||
mqtt_msg_count: u32,
|
||||
}
|
||||
|
||||
impl Default for DisplayState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status: String::new(),
|
||||
last_imu: None,
|
||||
last_error: None,
|
||||
mqtt_connected: false,
|
||||
mqtt_msg_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayState {
|
||||
fn apply_command(&mut self, cmd: DisplayCommand) {
|
||||
match cmd {
|
||||
DisplayCommand::SetImu(reading) => {
|
||||
self.last_imu = Some(reading);
|
||||
self.last_error = None;
|
||||
}
|
||||
DisplayCommand::SetStatus(s) => {
|
||||
self.status = s;
|
||||
}
|
||||
DisplayCommand::ShowError(e) => {
|
||||
self.last_error = Some(e);
|
||||
}
|
||||
DisplayCommand::SetMqttStatus { connected, msg_count } => {
|
||||
self.mqtt_connected = connected;
|
||||
self.mqtt_msg_count = msg_count;
|
||||
}
|
||||
DisplayCommand::Clear => {
|
||||
self.last_imu = None;
|
||||
self.last_error = None;
|
||||
self.status = String::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The display rendering task.
|
||||
/// Designed for 0.91" 128x32 slim OLED screen.
|
||||
#[embassy_executor::task]
|
||||
pub async fn display_task(i2c: I2cDevice) {
|
||||
info!("Display task starting...");
|
||||
|
||||
// Initialize SSD1306 display for 128x32 variant
|
||||
let interface = I2CDisplayInterface::new(i2c);
|
||||
let mut display = Ssd1306::new(interface, DisplaySize128x32, DisplayRotation::Rotate0)
|
||||
.into_buffered_graphics_mode();
|
||||
@@ -90,9 +30,6 @@ pub async fn display_task(i2c: I2cDevice) {
|
||||
}
|
||||
}
|
||||
|
||||
info!("SSD1306 display initialized (128x32)");
|
||||
|
||||
// Configure mousefood backend for ratatui
|
||||
let config = EmbeddedBackendConfig {
|
||||
flush_callback: Box::new(
|
||||
|d: &mut Ssd1306<_, _, BufferedGraphicsMode<DisplaySize128x32>>| {
|
||||
@@ -106,60 +43,68 @@ pub async fn display_task(i2c: I2cDevice) {
|
||||
let mut terminal = Terminal::new(backend).expect("terminal init failed");
|
||||
|
||||
let mut state = DisplayState::default();
|
||||
let mut orchestrator = Orchestrator::<Screen>::new();
|
||||
let rx = receiver();
|
||||
|
||||
info!("Display task entering render loop");
|
||||
// Register all pages
|
||||
orchestrator.register_page("imu".into(), Screen::Imu);
|
||||
orchestrator.register_page("mqtt".into(), Screen::Mqtt);
|
||||
orchestrator.register_page("status".into(), Screen::Status);
|
||||
|
||||
// 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());
|
||||
|
||||
loop {
|
||||
// Process all pending commands (non-blocking)
|
||||
while let Ok(cmd) = rx.try_receive() {
|
||||
state.apply_command(cmd);
|
||||
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 {
|
||||
for event in events {
|
||||
match event {
|
||||
ScreenEvent::NavigatePrev => {
|
||||
if let Some(current) = orchestrator.current_id() {
|
||||
let prev = prev_page_id(current.as_str());
|
||||
info!("Navigating to prev: {}", prev);
|
||||
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);
|
||||
let _ = orchestrator.navigate_to(next.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => state.apply_command(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
// Render current state
|
||||
render_frame(&mut terminal, &state);
|
||||
if let Some(current_screen) = orchestrator.current() {
|
||||
let focused = orchestrator.focus_manager().current();
|
||||
render_frame(&mut terminal, current_screen, focused, &state);
|
||||
}
|
||||
|
||||
// Wait before next refresh
|
||||
Timer::after(Duration::from_millis(REFRESH_INTERVAL_MS)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a single frame compactly (for 128x32 OLED)
|
||||
fn render_frame<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, state: &DisplayState) {
|
||||
let _ = terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(f.area());
|
||||
|
||||
// Header: condensed status + MQTT indicator
|
||||
let mqtt_indicator = if state.mqtt_connected { "M" } else { "m" };
|
||||
let header_title = format!(
|
||||
"[{}] {} #{}",
|
||||
mqtt_indicator,
|
||||
state.status.as_str(),
|
||||
state.mqtt_msg_count
|
||||
);
|
||||
|
||||
f.render_widget(Paragraph::new(header_title).style(Style::default().reversed()), chunks[0]);
|
||||
|
||||
// Body: minimal content (no borders, short text)
|
||||
let body_content = if let Some(ref err) = state.last_error {
|
||||
format!("ERR: {}", err.as_str())
|
||||
} else if let Some(ref imu) = state.last_imu {
|
||||
format!(
|
||||
"A:{:.1} {:.1} {:.1}\nG:{:.0} {:.0} {:.0}\nT:{:.1}C",
|
||||
imu.accel_g[0], imu.accel_g[1], imu.accel_g[2],
|
||||
imu.gyro_dps[0], imu.gyro_dps[1], imu.gyro_dps[2],
|
||||
imu.temp_c
|
||||
)
|
||||
} else {
|
||||
format!("Waiting for data...")
|
||||
};
|
||||
|
||||
f.render_widget(Paragraph::new(body_content), chunks[1]);
|
||||
});
|
||||
}
|
||||
|
||||
233
mqtt_display/src/display/tui.rs
Normal file
233
mqtt_display/src/display/tui.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
// src/display/tui.rs
|
||||
use crate::contracts::{DisplayCommand, ImuReading};
|
||||
use alloc::format;
|
||||
use heapless::String as HString;
|
||||
use pages_tui::prelude::*;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::Stylize,
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
};
|
||||
use log::info;
|
||||
|
||||
/// Focusable buttons available on every page
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum PageButton {
|
||||
Prev,
|
||||
Next,
|
||||
}
|
||||
|
||||
impl FocusId for PageButton {}
|
||||
|
||||
/// All available buttons for iteration
|
||||
const PAGE_BUTTONS: &[PageButton] = &[PageButton::Prev, PageButton::Next];
|
||||
|
||||
/// Display state shared between pages
|
||||
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,
|
||||
}
|
||||
|
||||
impl Default for DisplayState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status: HString::new(),
|
||||
last_imu: None,
|
||||
last_error: None,
|
||||
mqtt_connected: false,
|
||||
mqtt_msg_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayState {
|
||||
pub fn apply_command(&mut self, cmd: DisplayCommand) {
|
||||
match cmd {
|
||||
DisplayCommand::SetImu(reading) => {
|
||||
self.last_imu = Some(reading);
|
||||
self.last_error = None;
|
||||
}
|
||||
DisplayCommand::SetStatus(s) => self.status = s,
|
||||
DisplayCommand::ShowError(e) => self.last_error = Some(e),
|
||||
DisplayCommand::SetMqttStatus { connected, msg_count } => {
|
||||
self.mqtt_connected = connected;
|
||||
self.mqtt_msg_count = msg_count;
|
||||
}
|
||||
DisplayCommand::Clear => {
|
||||
self.last_imu = None;
|
||||
self.last_error = None;
|
||||
self.status = HString::new();
|
||||
}
|
||||
DisplayCommand::PushKey(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Screen {
|
||||
Imu,
|
||||
Mqtt,
|
||||
Status,
|
||||
}
|
||||
|
||||
/// Events emitted by screens for navigation
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ScreenEvent {
|
||||
NavigatePrev,
|
||||
NavigateNext,
|
||||
}
|
||||
|
||||
impl Component for Screen {
|
||||
type Action = ComponentAction;
|
||||
type Event = ScreenEvent;
|
||||
type Focus = PageButton;
|
||||
|
||||
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)),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn targets(&self) -> &[Self::Focus] {
|
||||
// Every page has Prev and Next buttons
|
||||
PAGE_BUTTONS
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> Result<(), ComponentError> {
|
||||
info!("Entered screen: {:?}", self);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_exit(&mut self) -> Result<(), ComponentError> {
|
||||
info!("Exited screen: {:?}", self);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ============ PAGE ORDER ============
|
||||
|
||||
const PAGE_ORDER: &[&str] = &["imu", "mqtt", "status"];
|
||||
|
||||
pub fn next_page_id(current: &str) -> &'static str {
|
||||
let idx = PAGE_ORDER.iter().position(|&p| p == current).unwrap_or(0);
|
||||
PAGE_ORDER[(idx + 1) % PAGE_ORDER.len()]
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
// ============ RENDERING ============
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_frame<B: ratatui::backend::Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
current_screen: &Screen,
|
||||
focused_button: Option<&PageButton>,
|
||||
state: &DisplayState,
|
||||
) {
|
||||
let _ = terminal.draw(|f| {
|
||||
// Handle errors with priority
|
||||
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());
|
||||
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]);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user