fpick 0.8.1

Interactive file picker
use std::str::Chars;

use crate::action_menu::MenuAction;
use crate::appdata::WindowFocus;
use crate::numbers::{ClampNumExt, MyIntExt};
use ratatui::{prelude::*, widgets::*};
use ratatui::{
    prelude::{Alignment, Frame},
    style::{Color, Style},
    widgets::{Block, BorderType, Borders, Paragraph},
};

use crate::app::App;
use crate::tree::TreeNode;

pub fn render(app: &mut App, frame: &mut Frame) {
    let area = frame.area();
    let middle_h = area.height - 3;

    let layout = Layout::default()
        .direction(Direction::Vertical)
        .constraints(vec![Constraint::Min(middle_h), Constraint::Max(3)])
        .split(area);

    render_dir_tree(app, frame, layout[0]);
    render_filter_panel(app, frame, layout[1]);
    if app.window_focus == WindowFocus::ActionMenu {
        render_action_popup(app, frame);
    } else if app.window_focus == WindowFocus::ActionMenuStep2 {
        render_action_popup_step2(app, frame);
    }
    if app.info_message.is_some() {
        render_info_popup(app, frame);
    }
    if app.error_message.is_some() {
        render_error_popup(app, frame);
    }
}

fn render_dir_tree(app: &mut App, frame: &mut Frame, area: Rect) {
    let list_items: Vec<ListItem> = app
        .child_tree_nodes
        .iter()
        .map(|it: &TreeNode| it.render_list_item())
        .collect();

    let max_title_w = area.width as usize - 2;
    let mut title_text = app.get_current_string_path();
    if title_text.len() > max_title_w {
        let split_pos = title_text
            .char_indices()
            .nth_back(max_title_w - 1 - 1)
            .unwrap()
            .0;
        title_text = format!("{}", &title_text[split_pos..]);
    }

    let title_block = Block::default()
        .title(title_text)
        .title_style(Style::new().bold())
        .title_alignment(Alignment::Left)
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded);

    let widget = List::new(list_items)
        .block(title_block)
        .style(Style::default().fg(Color::White))
        .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
        .highlight_symbol(">> ");

    frame.render_stateful_widget(widget, area, &mut app.file_tree_state);
}

fn render_filter_panel(app: &App, frame: &mut Frame, area: Rect) {
    let p_text = format!("{}\u{2588}", app.filter_text);
    let title = Block::default()
        .title("Search")
        .title_style(Style::new().bold())
        .title_alignment(Alignment::Left)
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded);

    let widget = Paragraph::new(p_text)
        .block(title)
        .style(Style::default().fg(Color::LightYellow))
        .alignment(Alignment::Left);

    frame.render_widget(widget, area);
}

fn render_action_popup(app: &App, frame: &mut Frame) {
    let list_items: Vec<ListItem> = app
        .known_menu_actions
        .iter()
        .map(|it: &MenuAction| ListItem::new(it.name))
        .collect();
    let mut list_state = ListState::default().with_selected(Some(app.action_menu_cursor_y));
    let widget = List::new(list_items)
        .block(
            Block::default()
                .title("Run action")
                .borders(Borders::ALL)
                .bg(Color::DarkGray),
        )
        .style(Style::default().fg(Color::White).bg(Color::DarkGray))
        .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
        .highlight_symbol(">> ");

    let height = app.known_menu_actions.len() as u16 + 2;
    let width: u16 = app
        .known_menu_actions
        .iter()
        .map(|it: &MenuAction| it.name.len() as u16)
        .max()
        .unwrap_or(0)
        + 8;
    let area = centered_rect(width, height, frame.area());
    let buffer = frame.buffer_mut();
    Clear.render(area, buffer);
    frame.render_stateful_widget(widget, area, &mut list_state);
}

fn render_action_popup_step2(app: &App, frame: &mut Frame) {
    let p_line = render_action_popup_step2_line(app);

    let title = Block::default()
        .title(app.action_menu_title.as_str())
        .title_style(Style::new().bold())
        .title_alignment(Alignment::Left)
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .bg(Color::DarkGray);
    let widget = Paragraph::new(p_line)
        .wrap(Wrap { trim: false })
        .block(title)
        .alignment(Alignment::Left);

    let width = frame.area().width * 3 / 4;
    let height = 4;
    let area = centered_rect(width, height, frame.area());
    let buffer = frame.buffer_mut();
    Clear.render(area, buffer);
    frame.render_widget(widget, area);
}

fn render_error_popup(app: &App, frame: &mut Frame) {
    if app.error_message.is_none() {
        return;
    }
    let error_message: String = app.error_message.clone().unwrap();

    let title_block = Block::default()
        .title("Error")
        .title_style(Style::new().bold())
        .title_alignment(Alignment::Center)
        .borders(Borders::ALL)
        .bg(Color::Red)
        .padding(Padding::bottom(1))
        .border_type(BorderType::Rounded);
    let error_window = Paragraph::new(error_message)
        .wrap(Wrap { trim: false })
        .block(title_block)
        .style(Style::default().fg(Color::White));
    let ok_label = Paragraph::new("OK")
        .style(Style::default().bold().fg(Color::LightRed).bg(Color::White))
        .alignment(Alignment::Center);

    let width: u16 = (frame.area().width as f32 * 0.75f32) as u16;
    let height: u16 = frame.area().height / 2;
    let area = centered_rect(width, height, frame.area());
    let ok_label_area = Rect {
        x: area.x + 1,
        y: area.y + area.height - 2,
        width: area.width - 2,
        height: 1,
    };
    Clear.render(area, frame.buffer_mut());
    frame.render_widget(error_window, area);
    frame.render_widget(ok_label, ok_label_area);
}

fn render_info_popup(app: &App, frame: &mut Frame) {
    if app.info_message.is_none() {
        return;
    }

    let width: u16 = frame.area().width.fraction(0.75);
    let info_message: String = app.info_message.clone().unwrap();
    let wrapped_lines = textwrap::wrap(info_message.as_str(), (width - 3) as usize);
    let wrapped_message = wrapped_lines.join("\n");
    let skipped_lines = wrapped_message
        .lines()
        .into_iter()
        .map(|s| s.to_string())
        .skip(app.info_message_scroll)
        .collect::<Vec<String>>();
    let display_message: String = skipped_lines.join("\n");
    let max_height: u16 = frame.area().height.fraction(0.75);
    let text_height: u16 = wrapped_message
        .lines()
        .count()
        .clamp_min(5)
        .clamp_max(max_height.into()) as u16;

    let title_block = Block::default()
        .title("Info")
        .title_style(Style::new().bold())
        .title_alignment(Alignment::Center)
        .borders(Borders::ALL)
        .bg(Color::Blue)
        .padding(Padding::bottom(1))
        .border_type(BorderType::Rounded);
    let popup_window = Paragraph::new(Text::raw(display_message))
        .wrap(Wrap { trim: false })
        .block(title_block)
        .style(Style::default().fg(Color::White));
    let ok_label = Paragraph::new("OK")
        .style(
            Style::default()
                .bold()
                .fg(Color::LightBlue)
                .bg(Color::White),
        )
        .alignment(Alignment::Center);

    let area = centered_rect(width, text_height + 3, frame.area());
    let ok_label_area = Rect {
        x: area.x + 1,
        y: area.y + area.height - 2,
        width: area.width - 2,
        height: 1,
    };
    Clear.render(area, frame.buffer_mut());
    frame.render_widget(popup_window, area);
    frame.render_widget(ok_label, ok_label_area);
}

fn centered_rect(w: u16, h: u16, r: Rect) -> Rect {
    let x_gap = (r.width as i32 - w as i32).clamp_min(0) / 2;
    let y_gap = (r.height as i32 - h as i32).clamp_min(0) / 2;
    Rect {
        x: r.x + x_gap as u16,
        y: r.y + y_gap as u16,
        width: w,
        height: h,
    }
}

fn render_action_popup_step2_line(app: &App) -> Line {
    let cx = app.action_menu_cursor_x;
    let chars: Chars<'_> = app.action_menu_buffer.chars();
    if cx >= chars.clone().count() {
        return Line::from(vec![
            Span::styled(
                app.action_menu_buffer.clone(),
                Style::default().fg(Color::White),
            ),
            Span::styled("", Style::default().fg(Color::White)),
        ]);
    }
    let buffer_pre: String = chars.clone().take(cx).collect::<String>();
    let highlighted: String = chars.clone().skip(cx).take(1).collect::<String>();
    let buffer_post: String = chars.skip(cx + 1).collect::<String>();
    if highlighted == " " {
        return Line::from(vec![
            Span::styled(buffer_pre, Style::default().fg(Color::White)),
            Span::styled("", Style::default().fg(Color::White)),
            Span::styled(buffer_post, Style::default().fg(Color::White)),
        ]);
    }
    Line::from(vec![
        Span::styled(buffer_pre, Style::default().fg(Color::White)),
        Span::styled(
            highlighted,
            Style::default().fg(Color::Black).bg(Color::White),
        ),
        Span::styled(buffer_post, Style::default().fg(Color::White)),
    ])
}