aboutsummaryrefslogtreecommitdiff
path: root/src/ui/release_notes.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/release_notes.rs')
-rw-r--r--src/ui/release_notes.rs289
1 files changed, 50 insertions, 239 deletions
diff --git a/src/ui/release_notes.rs b/src/ui/release_notes.rs
index 5274739c..5f6dc078 100644
--- a/src/ui/release_notes.rs
+++ b/src/ui/release_notes.rs
@@ -1,250 +1,15 @@
use ratatui::{
- layout::{Constraint, Layout, Rect},
+ layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::{Paragraph, Wrap},
- Frame,
};
-use super::scrollbar::{release_notes_scrollbar_rect, render_scrollbar};
-use super::widgets::{
- action_button_width, modal_stack_areas, panel_contrast_fg, render_action_button,
- render_modal_header, render_modal_shell,
-};
-use crate::app::{
- state::{Palette, ProductAnnouncementState, ReleaseNotesState},
- AppState,
-};
+use crate::app::state::{Palette, ProductAnnouncementState, ReleaseNotesState};
pub(crate) const RELEASE_NOTES_MODAL_SIZE: (u16, u16) = (80, 24);
pub(crate) const PRODUCT_ANNOUNCEMENT_MODAL_SIZE: (u16, u16) = (88, 24);
-pub(super) fn render_release_notes_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
- let Some(notes) = &app.release_notes else {
- return;
- };
-
- super::dim_background(frame, area);
-
- let Some(inner) = render_modal_shell(
- frame,
- area,
- RELEASE_NOTES_MODAL_SIZE.0,
- RELEASE_NOTES_MODAL_SIZE.1,
- &app.palette,
- ) else {
- return;
- };
- if inner.height < 8 || inner.width < 20 {
- return;
- }
-
- let stack = modal_stack_areas(inner, 2, 1, 0, 1);
- let header_rows =
- Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas::<2>(stack.header);
-
- let header_title_area = Rect::new(
- header_rows[0].x + 1,
- header_rows[0].y,
- header_rows[0].width.saturating_sub(2),
- header_rows[0].height,
- );
- let header_subtitle_area = Rect::new(
- header_rows[1].x + 1,
- header_rows[1].y,
- header_rows[1].width.saturating_sub(2),
- header_rows[1].height,
- );
-
- render_modal_header(
- frame,
- header_title_area,
- &format!("v{}", notes.version),
- &app.palette,
- );
- let subtitle = if notes.preview {
- "update ready"
- } else {
- "what's new in this release"
- };
- frame.render_widget(
- Paragraph::new(subtitle).style(Style::default().fg(app.palette.overlay1)),
- header_subtitle_area,
- );
- render_action_button(
- frame,
- release_notes_close_button_rect(header_rows[0]),
- Some("esc"),
- "close",
- Style::default()
- .fg(panel_contrast_fg(&app.palette))
- .bg(app.palette.accent)
- .add_modifier(Modifier::BOLD),
- );
-
- let notes_body = stack.content;
- let display_lines =
- release_notes_display_lines(notes, &app.update_install_command, &app.palette);
- let metrics = crate::pane::ScrollMetrics {
- offset_from_bottom: app.release_notes_max_scroll().saturating_sub(notes.scroll) as usize,
- max_offset_from_bottom: app.release_notes_max_scroll() as usize,
- viewport_rows: notes_body.height.max(1) as usize,
- };
- let track = release_notes_scrollbar_rect(notes_body, metrics);
- let notes_text_area = track
- .map(|_| {
- Rect::new(
- notes_body.x,
- notes_body.y,
- notes_body.width.saturating_sub(1),
- notes_body.height,
- )
- })
- .unwrap_or(notes_body);
-
- let body = Paragraph::new(
- display_lines
- .into_iter()
- .map(|(_, line)| line)
- .collect::<Vec<_>>(),
- )
- .wrap(Wrap { trim: false })
- .scroll((notes.scroll, 0));
- frame.render_widget(body, notes_text_area);
- if let Some(track) = track {
- render_scrollbar(
- frame,
- metrics,
- track,
- app.palette.overlay0,
- app.palette.overlay1,
- "▐",
- );
- }
-
- frame.render_widget(
- Paragraph::new(Line::from(vec![
- Span::styled(" scroll ", Style::default().fg(app.palette.overlay0)),
- Span::styled("wheel ↑↓", Style::default().fg(app.palette.text)),
- Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
- Span::styled("close", Style::default().fg(app.palette.overlay0)),
- Span::styled(" esc / enter ", Style::default().fg(app.palette.text)),
- ])),
- stack.footer.unwrap_or_default(),
- );
-}
-
-pub(super) fn render_product_announcement_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
- let Some(announcement) = &app.product_announcement else {
- return;
- };
-
- super::dim_background(frame, area);
-
- let Some(inner) = render_modal_shell(
- frame,
- area,
- PRODUCT_ANNOUNCEMENT_MODAL_SIZE.0,
- PRODUCT_ANNOUNCEMENT_MODAL_SIZE.1,
- &app.palette,
- ) else {
- return;
- };
- if inner.height < 8 || inner.width < 20 {
- return;
- }
-
- let stack = modal_stack_areas(inner, 2, 1, 0, 1);
- let header_rows =
- Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas::<2>(stack.header);
-
- let header_title_area = Rect::new(
- header_rows[0].x + 1,
- header_rows[0].y,
- header_rows[0].width.saturating_sub(2),
- header_rows[0].height,
- );
- let header_subtitle_area = Rect::new(
- header_rows[1].x + 1,
- header_rows[1].y,
- header_rows[1].width.saturating_sub(2),
- header_rows[1].height,
- );
-
- render_modal_header(frame, header_title_area, &announcement.title, &app.palette);
- let subtitle = if announcement.preview {
- "product announcement preview"
- } else {
- "product announcement"
- };
- frame.render_widget(
- Paragraph::new(format!("{subtitle} · v{}", announcement.version))
- .style(Style::default().fg(app.palette.overlay1)),
- header_subtitle_area,
- );
- render_action_button(
- frame,
- release_notes_close_button_rect(header_rows[0]),
- Some("esc"),
- "close",
- Style::default()
- .fg(panel_contrast_fg(&app.palette))
- .bg(app.palette.accent)
- .add_modifier(Modifier::BOLD),
- );
-
- let body_rect = stack.content;
- let metrics = crate::pane::ScrollMetrics {
- offset_from_bottom: app
- .product_announcement_max_scroll()
- .saturating_sub(announcement.scroll) as usize,
- max_offset_from_bottom: app.product_announcement_max_scroll() as usize,
- viewport_rows: body_rect.height.max(1) as usize,
- };
- let track = release_notes_scrollbar_rect(body_rect, metrics);
- let text_area = track
- .map(|_| {
- Rect::new(
- body_rect.x,
- body_rect.y,
- body_rect.width.saturating_sub(1),
- body_rect.height,
- )
- })
- .unwrap_or(body_rect);
-
- let body = Paragraph::new(
- product_announcement_display_lines(announcement, &app.palette)
- .into_iter()
- .map(|(_, line)| line)
- .collect::<Vec<_>>(),
- )
- .wrap(Wrap { trim: false })
- .scroll((announcement.scroll, 0));
- frame.render_widget(body, text_area);
- if let Some(track) = track {
- render_scrollbar(
- frame,
- metrics,
- track,
- app.palette.overlay0,
- app.palette.overlay1,
- "▐",
- );
- }
-
- frame.render_widget(
- Paragraph::new(Line::from(vec![
- Span::styled(" scroll ", Style::default().fg(app.palette.overlay0)),
- Span::styled("wheel ↑↓", Style::default().fg(app.palette.text)),
- Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
- Span::styled("close", Style::default().fg(app.palette.overlay0)),
- Span::styled(" esc / enter ", Style::default().fg(app.palette.text)),
- ])),
- stack.footer.unwrap_or_default(),
- );
-}
-
fn release_notes_inline_spans<'a>(
text: &str,
base_style: Style,
@@ -428,6 +193,53 @@ pub(crate) fn product_announcement_display_lines<'a>(
release_notes_lines(announcement.body.as_str(), p)
}
+fn display_lines_scroll_metrics(
+ lines: &[(usize, Line<'_>)],
+ scroll: u16,
+ body: Rect,
+) -> crate::pane::ScrollMetrics {
+ let viewport_rows = body.height.max(1) as usize;
+ let rows_for_width = |width: u16| release_notes_wrapped_line_count(lines, width.max(1));
+ let full_width = body.width.max(1);
+ let initial_rows = rows_for_width(full_width);
+ let wrap_width = if initial_rows > viewport_rows && full_width > 1 {
+ full_width.saturating_sub(1)
+ } else {
+ full_width
+ };
+ let max_offset_from_bottom = rows_for_width(wrap_width).saturating_sub(viewport_rows);
+ crate::pane::ScrollMetrics {
+ offset_from_bottom: max_offset_from_bottom.saturating_sub(usize::from(scroll)),
+ max_offset_from_bottom,
+ viewport_rows,
+ }
+}
+
+pub(crate) fn release_notes_scroll_metrics(
+ notes: &ReleaseNotesState,
+ install_command: &str,
+ body: Rect,
+ p: &Palette,
+) -> crate::pane::ScrollMetrics {
+ display_lines_scroll_metrics(
+ &release_notes_display_lines(notes, install_command, p),
+ notes.scroll,
+ body,
+ )
+}
+
+pub(crate) fn product_announcement_scroll_metrics(
+ announcement: &ProductAnnouncementState,
+ body: Rect,
+ p: &Palette,
+) -> crate::pane::ScrollMetrics {
+ display_lines_scroll_metrics(
+ &product_announcement_display_lines(announcement, p),
+ announcement.scroll,
+ body,
+ )
+}
+
pub(crate) fn release_notes_wrapped_line_count(lines: &[(usize, Line<'_>)], width: u16) -> usize {
Paragraph::new(
lines
@@ -440,8 +252,7 @@ pub(crate) fn release_notes_wrapped_line_count(lines: &[(usize, Line<'_>)], widt
}
pub(crate) fn release_notes_close_button_rect(area: Rect) -> Rect {
- let width = action_button_width(Some("esc"), "close");
- Rect::new(area.x + area.width.saturating_sub(width), area.y, width, 1)
+ super::widgets::close_button_rect(area)
}
#[cfg(test)]