use crate::ui;
use crate::ui::tui::Msg;
use crate::ui::tui::app::port::UserEvent;
use crate::ui::tui::config::{CommonAction, SpecialAction};
use crate::ui::tui::utils::flex_radio;
use strum_macros::FromRepr;
use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::props::{
Alignment, BorderSides, BorderType, Borders, Color, Layout, PropPayload, PropValue,
};
use tuirealm::ratatui::layout::Rect;
use tuirealm::{AttrValue, Attribute, Component, Event, Frame, MockComponent, Props, State, props};
#[derive(FromRepr, PartialEq, Clone, Copy)]
#[repr(u8)]
pub enum ViewSize {
Default,
Expand,
Compacted,
}
impl From<ViewSize> for AttrValue {
fn from(layout: ViewSize) -> Self {
AttrValue::Payload(PropPayload::One(PropValue::U8(layout as u8)))
}
}
impl From<AttrValue> for ViewSize {
fn from(value: AttrValue) -> Self {
ViewSize::from_repr(value.unwrap_payload().unwrap_one().unwrap_u8())
.expect("invalid attribute value")
}
}
pub struct TabWindow {
props: Props,
choices: flex_radio::Radio,
active_idx: Option<usize>,
visible_idx: usize,
windows: Vec<Box<dyn Component<Msg, UserEvent>>>,
view_size: ViewSize,
on_rewind: Option<fn(Direction) -> Msg>,
}
impl TabWindow {
pub fn new(
title: &str,
tabs: &[&str],
windows: Vec<Box<dyn Component<Msg, UserEvent>>>,
msg_on_rewind: Option<fn(Direction) -> Msg>,
) -> Self {
debug_assert!(tabs.len() == windows.len());
let choices = flex_radio::Radio::default()
.borders(
Borders::default()
.modifiers(BorderType::Rounded)
.color(Color::LightGreen),
)
.foreground(Color::LightGreen)
.title(title, Alignment::Center)
.rewind(msg_on_rewind.is_none())
.choices(tabs);
let tabs: Vec<_> = windows;
let this = Self {
active_idx: None,
visible_idx: 0,
choices,
windows: tabs,
props: Props::default(),
view_size: ViewSize::Default,
on_rewind: msg_on_rewind,
};
this.background(Color::Yellow)
.foreground(Color::Yellow)
.layout(
Layout::default()
.direction(tuirealm::ratatui::layout::Direction::Vertical)
.constraints(
[
tuirealm::ratatui::layout::Constraint::Length(3),
tuirealm::ratatui::layout::Constraint::Min(3),
]
.as_ref(),
),
)
}
}
impl TabWindow {
pub const VIEW_SIZE_ATTR: Attribute = Attribute::Custom("VIEW_SIZE");
pub const RESET_CHOICE_ATTR: Attribute = Attribute::Custom("RESET_CHOICE");
pub const ACTIVATE_TAB: Attribute = Attribute::Custom("ACTIVATE_TAB");
pub fn foreground(mut self, fg: Color) -> Self {
self.attr(Attribute::Foreground, AttrValue::Color(fg));
self
}
pub fn background(mut self, bg: Color) -> Self {
self.attr(Attribute::Background, AttrValue::Color(bg));
self
}
pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
self.attr(
Attribute::Title,
AttrValue::Title((t.as_ref().to_string(), a)),
);
self
}
pub fn layout(mut self, layout: Layout) -> Self {
self.attr(Attribute::Layout, AttrValue::Layout(layout));
self
}
pub fn windows(mut self, tabs: Vec<Box<dyn Component<Msg, UserEvent>>>) -> Self {
self.windows = tabs;
self
}
fn set_active_idx(&mut self, idx: usize) {
self.attr(Attribute::Focus, AttrValue::Flag(false));
self.active_idx = Some(idx);
self.visible_idx = idx;
if let Some(new_active_tab) = self.active_window_mut() {
new_active_tab.attr(Attribute::Focus, AttrValue::Flag(true));
}
}
fn deactivate_window(&mut self) {
if let Some(old_active_tab) = self.active_window_mut() {
old_active_tab.attr(Attribute::Focus, AttrValue::Flag(false));
}
self.active_idx = None;
}
fn active_window(&self) -> Option<&dyn Component<Msg, UserEvent>> {
self.windows.get(self.active_idx?).map(|b| &**b)
}
fn active_window_mut(&mut self) -> Option<&mut (dyn Component<Msg, UserEvent> + 'static)> {
self.windows
.get_mut(self.active_idx?)
.map(move |b| &mut **b)
}
fn visible_window(&self) -> Option<&dyn Component<Msg, UserEvent>> {
self.windows.get(self.visible_idx).map(|b| &**b)
}
fn visible_window_mut(&mut self) -> Option<&mut (dyn Component<Msg, UserEvent> + 'static)> {
self.windows
.get_mut(self.visible_idx)
.map(move |b| &mut **b)
}
}
impl MockComponent for TabWindow {
fn view(&mut self, frame: &mut Frame, area: Rect) {
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) != AttrValue::Flag(true) {
return;
}
let borders = self
.props
.get_or(
Attribute::Borders,
AttrValue::Borders(Borders::default().sides(BorderSides::NONE)),
)
.unwrap_borders();
let focus = self
.props
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
self.choices.attr(Attribute::Focus, AttrValue::Flag(focus));
let title = self.props.get(Attribute::Title).map(|x| x.unwrap_title());
let div = tui_realm_stdlib::utils::get_block(borders, title.as_ref(), focus, None);
frame.render_widget(div, area);
if let Some(layout) = self.props.get(Attribute::Layout).map(|x| x.unwrap_layout()) {
if self.view_size == ViewSize::Compacted {
self.choices.view(frame, area);
} else {
let chunks = layout.chunks(area);
debug_assert!(chunks.len() == 2);
self.choices.view(frame, chunks[0]);
if let Some(window) = self.visible_window_mut() {
window.view(frame, chunks[1]);
}
}
}
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.props.get(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
if matches!(attr, Attribute::Focus) && value == AttrValue::Flag(false) {
self.deactivate_window();
}
match attr {
Self::VIEW_SIZE_ATTR => {
self.view_size = crate::ui::tui::utils::tab::ViewSize::from(value);
match self.view_size {
ViewSize::Default | ViewSize::Expand => {
self.choices.states.set_horizontal_render()
}
ViewSize::Compacted => self.choices.states.set_vertical_layout(),
}
}
Self::RESET_CHOICE_ATTR => {
debug_assert!(matches!(
value,
AttrValue::Direction(props::Direction::Left)
| AttrValue::Direction(props::Direction::Right)
));
match value {
AttrValue::Direction(props::Direction::Left) => {
self.choices.states.select(0);
}
AttrValue::Direction(props::Direction::Right) => {
self.choices
.states
.select(self.choices.states.choices.len() - 1);
}
_ => {}
}
}
Self::ACTIVATE_TAB => {
let res = self.perform(Cmd::Submit);
if let CmdResult::Submit(state) = res {
let tab_idx = state.unwrap_one().unwrap_usize();
self.set_active_idx(tab_idx);
}
}
Attribute::Custom(_) => {
for comp in self.windows.iter_mut() {
comp.attr(attr, value.clone());
}
}
_ => {
self.props.set(attr, value.clone());
}
}
}
fn state(&self) -> State {
if let Some(window) = self.visible_window() {
return window.state();
}
self.choices.state()
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
if self.active_window().is_some() {
return CmdResult::None;
}
self.choices.perform(cmd)
}
}
impl Component<Msg, UserEvent> for TabWindow {
fn on(&mut self, ev: Event<UserEvent>) -> Option<Msg> {
let cmd_res = match ev {
Event::Keyboard(key_event) => {
let keymap = &ui::config::current().tui_keymap;
if let Some(action) = keymap.get_common(&key_event) {
match action {
CommonAction::Left => self.perform(Cmd::Move(Direction::Left)),
CommonAction::Right => self.perform(Cmd::Move(Direction::Right)),
CommonAction::Submit => {
let res = self.perform(Cmd::Submit);
if let CmdResult::Submit(state) = res {
let tab_idx = state.unwrap_one().unwrap_usize();
self.set_active_idx(tab_idx);
return Some(Msg::None);
}
CmdResult::None
}
_ => CmdResult::None,
}
} else if let Some(SpecialAction::SwitchWindowTab) = keymap.get_special(&key_event)
{
self.deactivate_window();
self.attr(Attribute::Focus, AttrValue::Flag(true));
self.perform(Cmd::Move(Direction::Right))
} else {
CmdResult::None
}
}
Event::User(_) | Event::Tick => {
for (i, window) in self.windows.iter_mut().enumerate() {
if !self.active_idx.map(|idx| idx == i).unwrap_or_default() {
_ = window.on(ev.clone());
}
}
CmdResult::None
}
_ => CmdResult::None,
};
if let CmdResult::Invalid(Cmd::Move(dir)) = cmd_res {
return self.on_rewind.map(|cb| cb(dir));
}
if let Some(window) = self.active_window_mut() {
return window.on(ev);
}
Some(Msg::None)
}
}