use std::any::Any;
use crossterm::event::KeyEvent;
use ratatui::layout::Rect;
use ratatui::Frame;
use super::chrome::ModalChrome;
use super::types::{Modal, ModalKind, ModalOutcome, ModalRenderCtx};
use crate::app::post_upgrade::changelog;
use crate::app::update_check::INSTALLED_VERSION;
use crate::app::App;
use crate::ui::update_check::{self as ui_update, PostUpgradeOccasion, PostUpgradeReport};
use crate::ui::{ModalResponse, PROSE_CONTENT_WIDTH};
pub struct PostUpgradeModal {
chrome: ModalChrome,
notes: Option<Vec<String>>,
occasion: PostUpgradeOccasion,
}
impl PostUpgradeModal {
pub(crate) fn for_upgrade() -> Option<Self> {
Some(Self::new(
Some(changelog::notes_for_version(INSTALLED_VERSION)?),
PostUpgradeOccasion::Upgrade,
))
}
pub(crate) fn on_demand() -> Self {
Self::new(
changelog::notes_for_version(INSTALLED_VERSION),
PostUpgradeOccasion::OnDemand,
)
}
fn new(notes: Option<Vec<String>>, occasion: PostUpgradeOccasion) -> Self {
Self {
chrome: ModalChrome::new(ModalKind::Normal, true)
.with_max_content_width(PROSE_CONTENT_WIDTH),
notes,
occasion,
}
}
fn report(&self) -> PostUpgradeReport<'_> {
match &self.notes {
Some(notes) => PostUpgradeReport::Found { notes },
None => PostUpgradeReport::NotFound,
}
}
fn resolve(response: ModalResponse) -> ModalOutcome {
match response {
ModalResponse::Continue => ModalOutcome::Continue,
ModalResponse::Cancelled | ModalResponse::ButtonPressed(_) => ModalOutcome::Close,
}
}
}
impl Modal for PostUpgradeModal {
fn render(&mut self, frame: &mut Frame<'_>, area: Rect, ctx: &ModalRenderCtx<'_>) {
let body = ui_update::post_upgrade_body_lines(
ctx.theme,
self.occasion,
self.report(),
INSTALLED_VERSION,
);
self.chrome
.render(frame, area, ctx, "Release notes", &body, &[]);
}
fn handle_key(
&mut self,
key: KeyEvent,
_app: &mut App,
_doc_height: usize,
_doc_width: usize,
) -> ModalOutcome {
Self::resolve(self.chrome.on_key(&key, 0))
}
fn handle_wheel(&mut self, delta: i32) {
self.chrome.on_wheel(delta);
}
fn handle_click(&mut self, col: u16, row: u16, _app: &mut App) -> ModalOutcome {
Self::resolve(self.chrome.on_click(col, row))
}
fn kind(&self) -> ModalKind {
self.chrome.kind()
}
fn dismissable(&self) -> bool {
self.chrome.dismissable()
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::*;
use crate::app::test_utils::make_app;
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn with_notes() -> PostUpgradeModal {
PostUpgradeModal::new(
Some(vec!["- a thing".to_owned()]),
PostUpgradeOccasion::Upgrade,
)
}
#[test]
fn esc_dismisses() {
let mut app = make_app();
let mut modal = with_notes();
assert!(matches!(
modal.handle_key(key(KeyCode::Esc), &mut app, 24, 80),
ModalOutcome::Close
));
}
#[test]
fn enter_does_nothing_since_there_is_nothing_to_press() {
let mut app = make_app();
let mut modal = with_notes();
assert!(matches!(
modal.handle_key(key(KeyCode::Enter), &mut app, 24, 80),
ModalOutcome::Continue
));
}
#[test]
fn a_missing_section_and_an_empty_one_are_different_states() {
assert!(matches!(
PostUpgradeModal::new(None, PostUpgradeOccasion::OnDemand).report(),
PostUpgradeReport::NotFound
));
assert!(matches!(
PostUpgradeModal::new(Some(Vec::new()), PostUpgradeOccasion::OnDemand).report(),
PostUpgradeReport::Found { .. }
));
}
#[test]
fn the_on_demand_opening_always_builds_a_modal() {
let modal = PostUpgradeModal::on_demand();
assert!(modal.dismissable());
assert_eq!(modal.kind(), ModalKind::Normal);
}
#[test]
fn each_entry_point_carries_its_own_occasion() {
assert_eq!(
PostUpgradeModal::on_demand().occasion,
PostUpgradeOccasion::OnDemand
);
if let Some(modal) = PostUpgradeModal::for_upgrade() {
assert_eq!(modal.occasion, PostUpgradeOccasion::Upgrade);
}
}
}