use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph};
use super::{Component, RenderContext};
#[derive(Clone, Debug, PartialEq)]
pub enum TitleCardMessage {
SetTitle(String),
SetSubtitle(Option<String>),
SetPrefix(Option<String>),
SetSuffix(Option<String>),
SetTitleStyle(Style),
SetSubtitleStyle(Style),
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct TitleCardState {
title: String,
subtitle: Option<String>,
prefix: Option<String>,
suffix: Option<String>,
title_style: Style,
subtitle_style: Style,
bordered: bool,
disabled: bool,
}
impl Default for TitleCardState {
fn default() -> Self {
Self {
title: String::new(),
subtitle: None,
prefix: None,
suffix: None,
title_style: Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
subtitle_style: Style::default().fg(Color::DarkGray),
bordered: true,
disabled: false,
}
}
}
impl TitleCardState {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
..Self::default()
}
}
pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
self.subtitle = Some(subtitle.into());
self
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = Some(prefix.into());
self
}
pub fn with_suffix(mut self, suffix: impl Into<String>) -> Self {
self.suffix = Some(suffix.into());
self
}
pub fn with_title_style(mut self, style: Style) -> Self {
self.title_style = style;
self
}
pub fn with_subtitle_style(mut self, style: Style) -> Self {
self.subtitle_style = style;
self
}
pub fn with_bordered(mut self, bordered: bool) -> Self {
self.bordered = bordered;
self
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn title(&self) -> &str {
&self.title
}
pub fn subtitle(&self) -> Option<&str> {
self.subtitle.as_deref()
}
pub fn prefix(&self) -> Option<&str> {
self.prefix.as_deref()
}
pub fn suffix(&self) -> Option<&str> {
self.suffix.as_deref()
}
pub fn title_style(&self) -> Style {
self.title_style
}
pub fn subtitle_style(&self) -> Style {
self.subtitle_style
}
pub fn is_bordered(&self) -> bool {
self.bordered
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = title.into();
}
pub fn set_subtitle(&mut self, subtitle: Option<String>) {
self.subtitle = subtitle;
}
pub fn set_prefix(&mut self, prefix: Option<String>) {
self.prefix = prefix;
}
pub fn set_suffix(&mut self, suffix: Option<String>) {
self.suffix = suffix;
}
pub fn set_title_style(&mut self, style: Style) {
self.title_style = style;
}
pub fn set_subtitle_style(&mut self, style: Style) {
self.subtitle_style = style;
}
pub fn set_bordered(&mut self, bordered: bool) {
self.bordered = bordered;
}
pub fn set_disabled(&mut self, disabled: bool) {
self.disabled = disabled;
}
}
pub struct TitleCard;
impl Component for TitleCard {
type State = TitleCardState;
type Message = TitleCardMessage;
type Output = ();
fn init() -> Self::State {
TitleCardState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
TitleCardMessage::SetTitle(title) => state.title = title,
TitleCardMessage::SetSubtitle(subtitle) => state.subtitle = subtitle,
TitleCardMessage::SetPrefix(prefix) => state.prefix = prefix,
TitleCardMessage::SetSuffix(suffix) => state.suffix = suffix,
TitleCardMessage::SetTitleStyle(style) => state.title_style = style,
TitleCardMessage::SetSubtitleStyle(style) => state.subtitle_style = style,
}
None
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
crate::annotation::with_registry(|reg| {
reg.register(
ctx.area,
crate::annotation::Annotation::title_card("title_card")
.with_label(state.title.as_str())
.with_disabled(ctx.disabled),
);
});
let render_area = if state.bordered {
let border_style = if ctx.disabled {
ctx.theme.disabled_style()
} else {
ctx.theme.border_style()
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(border_style);
let inner = block.inner(ctx.area);
ctx.frame.render_widget(block, ctx.area);
inner
} else {
ctx.area
};
if render_area.height == 0 || render_area.width == 0 {
return;
}
let title_style = if ctx.disabled {
ctx.theme.disabled_style()
} else {
state.title_style
};
let subtitle_style = if ctx.disabled {
ctx.theme.disabled_style()
} else {
state.subtitle_style
};
let mut title_spans = Vec::new();
if let Some(prefix) = &state.prefix {
title_spans.push(Span::styled(prefix.as_str(), title_style));
}
title_spans.push(Span::styled(state.title.as_str(), title_style));
if let Some(suffix) = &state.suffix {
title_spans.push(Span::styled(suffix.as_str(), title_style));
}
let title_line = Line::from(title_spans);
let content_height = if state.subtitle.is_some() { 2 } else { 1 };
let vertical_offset = render_area.height.saturating_sub(content_height) / 2;
let title_area = Rect::new(
render_area.x,
render_area.y + vertical_offset,
render_area.width,
1.min(render_area.height.saturating_sub(vertical_offset)),
);
if title_area.height > 0 {
let title_paragraph = Paragraph::new(title_line).alignment(Alignment::Center);
ctx.frame.render_widget(title_paragraph, title_area);
}
if let Some(subtitle) = &state.subtitle {
let subtitle_y = render_area.y + vertical_offset + 1;
if subtitle_y < render_area.y + render_area.height {
let subtitle_area = Rect::new(render_area.x, subtitle_y, render_area.width, 1);
let subtitle_paragraph =
Paragraph::new(Span::styled(subtitle.as_str(), subtitle_style))
.alignment(Alignment::Center);
ctx.frame.render_widget(subtitle_paragraph, subtitle_area);
}
}
}
}
#[cfg(test)]
mod tests;