use crate::tui::core::{LinkZones, ScrollIden, UiAction};
use crate::tui::support::{UiExt as _, clamp_idx_in_len};
use crate::tui::view::comp::{self, ui_for_marker_section_str};
use crate::tui::view::support::RectExt as _;
use crate::tui::{AppState, TaskView, style};
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, HighlightSpacing, List, ListItem, ListState, Paragraph, StatefulWidget, Widget as _};
pub struct RunTasksView;
impl RunTasksView {
const TASKS_NAV_SCROLL_IDEN: ScrollIden = ScrollIden::TasksNav;
const SCROLL_IDENS: &[&ScrollIden] = &[&Self::TASKS_NAV_SCROLL_IDEN];
pub fn clear_scroll_idens(state: &mut AppState) {
state.clear_scroll_zone_areas(Self::SCROLL_IDENS);
TaskView::clear_scroll_idens(state);
}
}
impl StatefulWidget for RunTasksView {
type State = AppState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if state.tasks().is_empty() {
render_no_tasks(area, buf, state);
return;
}
let show_tasks_nav = state.tasks().len() > 1;
let tasks_nav_width = if show_tasks_nav { 20 } else { 0 };
let [nav_a, content_a] = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Max(tasks_nav_width), Constraint::Min(0)])
.spacing(1)
.areas(area);
let mut selection_in_view = false;
if let Some(UiAction::GoToTask { task_id }) = state.action() {
if let Some(task_idx) = state.tasks().iter().position(|t| t.id == *task_id) {
state.set_task_idx(Some(task_idx));
selection_in_view = true;
}
state.clear_action();
}
if show_tasks_nav {
render_tasks_nav(nav_a, buf, selection_in_view, state);
} else {
state.clear_scroll_zone_area(&RunTasksView::TASKS_NAV_SCROLL_IDEN);
}
TaskView.render(content_a, buf, state);
}
}
fn render_no_tasks(area: Rect, buf: &mut Buffer, state: &mut AppState) {
let area = area.x_h_margin(1);
if let Some(err_id) = state.current_run_item().and_then(|r| r.run().end_err_id) {
let mut link_zones = LinkZones::default();
link_zones.set_current_line(0);
let path_color = (state.debug_clr() != 0).then(|| ratatui::style::Color::Indexed(state.debug_clr()));
let mut lines =
super::comp::ui_for_err_with_hover(state.mm(), err_id, area.width.min(120), &mut link_zones, path_color);
let scroll: u16 = 0;
let zones = link_zones.into_zones();
let mut hovered_idx: Option<usize> = None;
let mut min_span_count = usize::MAX;
for (i, zone) in zones.iter().enumerate() {
if let Some(line) = lines.get_mut(zone.line_idx)
&& zone
.is_mouse_over(area, scroll, state.last_mouse_evt(), &mut line.spans)
.is_some() && zone.span_count < min_span_count
{
min_span_count = zone.span_count;
hovered_idx = Some(i);
}
}
if let Some(i) = hovered_idx {
let action = zones[i].action.clone();
let group_id = zones[i].group_id;
match group_id {
Some(gid) => {
for z in zones.iter().filter(|z| z.group_id == Some(gid)) {
if let Some(line) = lines.get_mut(z.line_idx)
&& let Some(hover_spans) = z.spans_slice_mut(&mut line.spans)
{
for span in hover_spans {
span.style.fg = Some(style::CLR_TXT_HOVER_TO_CLIP);
}
}
}
}
None => {
if let Some(line) = lines.get_mut(zones[i].line_idx)
&& let Some(hover_spans) = zones[i].spans_slice_mut(&mut line.spans)
{
for span in hover_spans {
span.style = style::style_text_path(true, None);
}
}
}
}
if state.is_mouse_up_only() && state.is_last_mouse_over(area) {
state.set_action(action);
state.clear_mouse_evts();
}
}
Paragraph::new(lines).render(area, buf);
}
else if let Some(run_skip_reason) = state.current_run_item().and_then(|r| r.run().end_skip_reason.as_ref()) {
let marker = ("â– Skip:", style::STL_SECTION_MARKER_SKIP);
let path_color = (state.debug_clr() != 0).then(|| ratatui::style::Color::Indexed(state.debug_clr()));
let line = ui_for_marker_section_str(run_skip_reason, marker, area.width, None, None, None, path_color);
Paragraph::new(line).render(area, buf);
} else {
Paragraph::new("").render(area, buf);
}
}
fn render_tasks_nav(area: Rect, buf: &mut Buffer, selection_in_view: bool, state: &mut AppState) {
const SCROLL_IDEN: ScrollIden = RunTasksView::TASKS_NAV_SCROLL_IDEN;
Block::new().bg(style::CLR_BKG_GRAY_DARKER).render(area, buf);
let [tasks_label_a, tasks_list_a] = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(1), Constraint::Fill(1), ])
.areas(area);
let before_line = Line::default().spans(vec![
Span::raw(" Tasks:"),
]);
before_line.style(style::STL_FIELD_LBL).render(tasks_label_a, buf);
state.set_scroll_area(SCROLL_IDEN, tasks_list_a);
let tasks_len = state.tasks().len();
let mut scroll = state.clamp_scroll(SCROLL_IDEN, tasks_len);
process_mouse_for_task_nav(state, tasks_list_a, scroll);
let tasks = state.tasks();
let task_sel_idx = state.task_idx().unwrap_or_default();
let is_mouse_in_nav = state.is_last_mouse_over(tasks_list_a);
let items: Vec<ListItem> = tasks
.iter()
.enumerate()
.map(|(idx, task)| {
let mut line = Line::from(task.ui_label(Some(" "), area.width, tasks_len));
if task_sel_idx == idx {
line = line.style(style::STL_NAV_ITEM_HIGHLIGHT);
line = line.x_fg(style::CLR_TXT_BLACK);
} else if is_mouse_in_nav && state.is_last_mouse_over(tasks_list_a.x_row((idx + 1) as u16 - scroll)) {
line = line.fg(style::CLR_TXT_HOVER);
}
ListItem::new(line)
})
.collect();
let item_count = items.len() as u16;
let list_w = List::new(items)
.highlight_spacing(HighlightSpacing::Always);
if selection_in_view {
let visible_top_idx = scroll as usize;
let area_height = tasks_list_a.height as usize;
let visible_end_idx = visible_top_idx + area_height.saturating_sub(1);
if task_sel_idx >= visible_top_idx && task_sel_idx <= visible_end_idx {
} else if task_sel_idx < visible_top_idx {
scroll = task_sel_idx as u16;
state.set_scroll(SCROLL_IDEN, scroll);
} else if task_sel_idx > visible_end_idx {
scroll = (task_sel_idx - area_height + 1) as u16;
state.set_scroll(SCROLL_IDEN, scroll);
}
}
let mut list_s = ListState::default().with_offset(scroll as usize);
StatefulWidget::render(list_w, tasks_list_a, buf, &mut list_s);
if item_count - scroll > tasks_list_a.height {
let bottom_ico = tasks_list_a.x_bottom_right(1, 1);
comp::ico_scroll_down().render(bottom_ico, buf);
}
if scroll > 0 && item_count > tasks_list_a.height - scroll {
let top_ico = tasks_list_a.x_top_right(1, 1);
comp::ico_scroll_up().render(top_ico, buf);
}
}
fn process_mouse_for_task_nav(state: &mut AppState, nav_a: Rect, scroll: u16) {
if let Some(mouse_evt) = state.mouse_evt()
&& mouse_evt.is_up()
&& mouse_evt.is_over(nav_a)
{
let new_idx = mouse_evt.y() - nav_a.y + scroll;
let new_idx = clamp_idx_in_len(new_idx as usize, state.tasks().len());
state.set_task_idx(Some(new_idx));
}
}