use super::super::theme::MissionControlTheme;
use super::{
bordered_block, overlay_block_with_theme, overlay_text_area, render_overlay_fill_with_theme,
};
use ratatui::{
Frame,
layout::{Rect, Size},
widgets::{Clear, List, ListItem, ListState},
};
use std::cell::RefCell;
use tui_scrollview::ScrollView;
use super::super::state::{MissionControlState, ModalKind};
pub(super) fn draw_top_modal(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
match state.top_modal() {
Some(ModalKind::SubagentViewer) => draw_subagent_viewer_modal(frame, area, state),
Some(ModalKind::SessionPicker) => draw_sessions_modal(frame, area, state),
Some(ModalKind::Skills) => draw_skills_modal(frame, area, state),
Some(ModalKind::Mcp) => draw_mcp_modal(frame, area, state),
Some(ModalKind::Tools) => draw_tools_modal(frame, area, state),
Some(ModalKind::Subagents) => draw_subagents_modal(frame, area, state),
Some(ModalKind::Models) => draw_models_modal(frame, area, state),
Some(ModalKind::Usage) => draw_usage_modal(frame, area, state),
Some(ModalKind::SystemPrompt) => draw_system_prompt_modal(frame, area, state),
Some(ModalKind::Rewind) => draw_rewind_modal(frame, area, state),
Some(ModalKind::ConnectProvider) => draw_connect_provider(frame, area, state),
Some(ModalKind::LogoutConfirmation) => draw_logout_confirmation(frame, area, state),
Some(ModalKind::LogoutPicker) => draw_logout_picker(frame, area, state),
Some(ModalKind::ModelPicker) => draw_model_picker(frame, area, state),
Some(ModalKind::ThemePicker) => draw_theme_picker(frame, area, state),
None => {}
}
}
pub(super) mod auth;
pub(super) mod mcp;
pub(super) mod model;
pub(super) mod models;
pub(super) mod rewind;
pub(super) mod sessions;
pub(super) mod skills;
pub(super) mod subagent_viewer;
pub(super) mod subagents;
pub(super) mod system_prompt;
pub(super) mod theme;
pub(super) mod tools;
pub(super) mod usage;
pub(crate) use auth::{
connect_provider_choice_list_area, connect_provider_custom_field_visible,
connect_provider_modal_visible, connect_provider_oauth_controls_visible,
connect_provider_oauth_fallback_visible, logout_confirmation_action_row_area,
logout_picker_list_area,
};
pub(super) use auth::{draw_connect_provider, draw_logout_confirmation, draw_logout_picker};
pub(super) use mcp::draw_mcp_modal;
pub(crate) use mcp::mcp_modal_list_area;
pub(super) use model::draw_model_picker;
pub(crate) use model::model_picker_list_area;
pub(super) use models::draw_models_modal;
pub(crate) use models::models_modal_list_area;
pub(super) use rewind::draw_rewind_modal;
pub(crate) use rewind::rewind_modal_text_area;
pub(super) use sessions::draw_sessions_modal;
pub(crate) use sessions::{
sessions_modal_areas, sessions_modal_list_area, sessions_modal_preview_area,
};
pub(super) use skills::draw_skills_modal;
pub(crate) use skills::skills_modal_list_area;
pub(super) use subagent_viewer::draw_subagent_viewer_modal;
pub(crate) use subagent_viewer::{subagent_viewer_tab_at, subagent_viewer_timeline_area};
pub(super) use subagents::draw_subagents_modal;
pub(crate) use subagents::subagents_modal_list_area;
pub(super) use system_prompt::draw_system_prompt_modal;
pub(crate) use system_prompt::system_prompt_text_area;
pub(super) use theme::draw_theme_picker;
pub(crate) use theme::{theme_picker_list_area, theme_picker_query_visible};
pub(super) use tools::draw_tools_modal;
pub(crate) use tools::tools_modal_list_area;
pub(super) use usage::draw_usage_modal;
pub(crate) use usage::usage_modal_text_area;
#[derive(Debug, Clone, Copy)]
pub(in crate::tui::render) struct ModalSpec {
min_width: u16,
min_height: u16,
width_ratio: (u16, u16),
height: ModalHeight,
}
#[derive(Debug, Clone, Copy)]
pub(in crate::tui::render) enum ModalHeight {
Ratio(u16, u16),
Fixed(u16),
}
pub(in crate::tui::render) const SYSTEM_PROMPT_MODAL: ModalSpec = ModalSpec {
min_width: 12,
min_height: 5,
width_ratio: (9, 10),
height: ModalHeight::Ratio(4, 5),
};
pub(super) const SESSIONS_MODAL: ModalSpec = ModalSpec {
min_width: 12,
min_height: 5,
width_ratio: (9, 10),
height: ModalHeight::Ratio(4, 5),
};
pub(in crate::tui::render) const PICKER_MODAL: ModalSpec = ModalSpec {
min_width: 12,
min_height: 5,
width_ratio: (4, 5),
height: ModalHeight::Ratio(3, 5),
};
pub(super) const CONFIRMATION_MODAL: ModalSpec = ModalSpec {
min_width: 12,
min_height: 5,
width_ratio: (3, 5),
height: ModalHeight::Fixed(7),
};
pub(super) const CONNECT_PROVIDER_MODAL: ModalSpec = ModalSpec {
min_width: 60,
min_height: 18,
width_ratio: (9, 10),
height: ModalHeight::Ratio(4, 5),
};
pub(in crate::tui::render) fn modal_rect(area: Rect, spec: ModalSpec) -> Option<Rect> {
if area.width < spec.min_width || area.height < spec.min_height {
return None;
}
let width = area
.width
.saturating_mul(spec.width_ratio.0)
.saturating_div(spec.width_ratio.1)
.clamp(spec.min_width, area.width);
let height = match spec.height {
ModalHeight::Ratio(numerator, denominator) => area
.height
.saturating_mul(numerator)
.saturating_div(denominator)
.clamp(spec.min_height, area.height),
ModalHeight::Fixed(height) => height.min(area.height).max(spec.min_height),
};
Some(Rect::new(
area.x + area.width.saturating_sub(width) / 2,
area.y + area.height.saturating_sub(height) / 2,
width,
height,
))
}
pub(super) fn modal_text_area(area: Rect, _title: &'static str, spec: ModalSpec) -> Option<Rect> {
let modal = modal_rect(area, spec)?;
Some(overlay_text_area(bordered_block().inner(modal)))
}
pub(super) fn draw_modal_panel(
frame: &mut Frame<'_>,
area: Rect,
title: &'static str,
spec: ModalSpec,
theme: MissionControlTheme,
) -> Option<Rect> {
let modal = modal_rect(area, spec)?;
frame.render_widget(Clear, modal);
let block = overlay_block_with_theme(theme, title);
let inner = block.inner(modal);
frame.render_widget(block, modal);
Some(render_overlay_fill_with_theme(frame, inner, theme))
}
pub(super) fn block_inner(area: Rect) -> Rect {
Rect::new(
area.x.saturating_add(1),
area.y.saturating_add(1),
area.width.saturating_sub(2),
area.height.saturating_sub(2),
)
}
pub(super) fn picker_list_area(text_area: Rect, header_rows: u16) -> Option<Rect> {
if text_area.width == 0 || text_area.height == 0 {
return None;
}
let list_y = text_area.y.saturating_add(header_rows);
let list_height = text_area
.y
.saturating_add(text_area.height)
.saturating_sub(list_y);
(list_height > 0).then_some(Rect::new(text_area.x, list_y, text_area.width, list_height))
}
pub(super) fn picker_modal_list_area(
area: Rect,
title: &'static str,
header_rows: u16,
) -> Option<Rect> {
let text_area = modal_text_area(area, title, PICKER_MODAL)?;
picker_list_area(text_area, header_rows)
}
pub(crate) fn settings_scope_visible(area: ratatui::layout::Rect) -> bool {
modal_text_area(area, "Settings", PICKER_MODAL)
.is_some_and(|text_area| text_area.width > 0 && text_area.height >= 2)
}
pub(super) fn render_picker_list(
frame: &mut Frame<'_>,
area: Rect,
items: Vec<ListItem<'static>>,
selected: usize,
scroll_state: &RefCell<tui_scrollview::ScrollViewState>,
theme: MissionControlTheme,
) {
if area.width == 0 || area.height == 0 {
return;
}
let content_height = super::usize_to_u16_saturating(items.len().max(usize::from(area.height)));
let content_width = super::scroll_view_content_width(area);
if content_width == 0 {
return;
}
let mut view = ScrollView::new(Size::new(content_width, content_height));
let mut list_state = ListState::default().with_selected(Some(selected));
view.render_stateful_widget(
List::new(items)
.style(theme.pane())
.highlight_style(theme.selection())
.highlight_symbol("> "),
Rect::new(0, 0, content_width, content_height),
&mut list_state,
);
let mut local_scroll_state = *scroll_state.borrow();
super::render_prepared_scroll_view_with_theme(
frame,
area,
view,
&mut local_scroll_state,
None,
theme,
);
}