use crate::event::Key;
use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::utils::char_width;
use crate::widget::layout::border::{draw_border, BorderType};
use crate::widget::theme::{DISABLED_FG, LIGHT_GRAY, MUTED_TEXT, SECONDARY_TEXT};
use crate::widget::traits::{RenderContext, View, WidgetProps, WidgetState};
use crate::{impl_styled_view, impl_widget_builders};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AlertLevel {
#[default]
Info,
Success,
Warning,
Error,
}
impl AlertLevel {
pub fn icon(&self) -> char {
match self {
AlertLevel::Info => 'ℹ',
AlertLevel::Success => '✓',
AlertLevel::Warning => '⚠',
AlertLevel::Error => '✗',
}
}
pub fn color(&self) -> Color {
match self {
AlertLevel::Info => Color::CYAN,
AlertLevel::Success => Color::GREEN,
AlertLevel::Warning => Color::YELLOW,
AlertLevel::Error => Color::RED,
}
}
pub fn bg_color(&self) -> Color {
match self {
AlertLevel::Info => Color::rgb(0, 30, 50),
AlertLevel::Success => Color::rgb(0, 35, 0),
AlertLevel::Warning => Color::rgb(50, 35, 0),
AlertLevel::Error => Color::rgb(50, 0, 0),
}
}
pub fn border_color(&self) -> Color {
match self {
AlertLevel::Info => Color::rgb(0, 100, 150),
AlertLevel::Success => Color::rgb(0, 120, 0),
AlertLevel::Warning => Color::rgb(180, 120, 0),
AlertLevel::Error => Color::rgb(150, 0, 0),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AlertVariant {
#[default]
Filled,
Outlined,
Minimal,
}
pub struct Alert {
message: String,
title: Option<String>,
level: AlertLevel,
variant: AlertVariant,
show_icon: bool,
dismissible: bool,
dismissed: bool,
custom_icon: Option<char>,
state: WidgetState,
props: WidgetProps,
}
impl Alert {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
title: None,
level: AlertLevel::default(),
variant: AlertVariant::default(),
show_icon: true,
dismissible: false,
dismissed: false,
custom_icon: None,
state: WidgetState::new(),
props: WidgetProps::new(),
}
}
pub fn info(message: impl Into<String>) -> Self {
Self::new(message).level(AlertLevel::Info)
}
pub fn success(message: impl Into<String>) -> Self {
Self::new(message).level(AlertLevel::Success)
}
pub fn warning(message: impl Into<String>) -> Self {
Self::new(message).level(AlertLevel::Warning)
}
pub fn error(message: impl Into<String>) -> Self {
Self::new(message).level(AlertLevel::Error)
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn level(mut self, level: AlertLevel) -> Self {
self.level = level;
self
}
pub fn variant(mut self, variant: AlertVariant) -> Self {
self.variant = variant;
self
}
pub fn icon(mut self, show: bool) -> Self {
self.show_icon = show;
self
}
pub fn custom_icon(mut self, icon: char) -> Self {
self.custom_icon = Some(icon);
self.show_icon = true;
self
}
pub fn dismissible(mut self, dismissible: bool) -> Self {
self.dismissible = dismissible;
self
}
pub fn is_dismissed(&self) -> bool {
self.dismissed
}
pub fn dismiss(&mut self) {
self.dismissed = true;
}
pub fn reset(&mut self) {
self.dismissed = false;
}
fn get_icon(&self) -> char {
self.custom_icon.unwrap_or_else(|| self.level.icon())
}
pub fn height(&self) -> u16 {
if self.dismissed {
return 0;
}
let has_title = self.title.is_some();
match self.variant {
AlertVariant::Filled | AlertVariant::Outlined => {
if has_title {
4 } else {
3 }
}
AlertVariant::Minimal => {
if has_title {
2
} else {
1
}
}
}
}
pub fn handle_key(&mut self, key: &Key) -> bool {
if self.dismissed || !self.dismissible {
return false;
}
match key {
Key::Char('x') | Key::Char('X') | Key::Escape => {
self.dismiss();
true
}
_ => false,
}
}
}
impl Default for Alert {
fn default() -> Self {
Self::new("Alert")
}
}
impl View for Alert {
crate::impl_view_meta!("Alert");
fn render(&self, ctx: &mut RenderContext) {
if self.dismissed {
return;
}
let area = ctx.area;
if area.width < 5 || area.height < 1 {
return;
}
let accent_color = self.level.color();
let bg_color = self.level.bg_color();
let border_color = self.level.border_color();
match self.variant {
AlertVariant::Filled => {
self.render_filled(ctx, accent_color, bg_color, border_color);
}
AlertVariant::Outlined => {
self.render_outlined(ctx, accent_color, border_color);
}
AlertVariant::Minimal => {
self.render_minimal(ctx, accent_color);
}
}
}
}
impl Alert {
fn render_filled(
&self,
ctx: &mut RenderContext,
accent_color: Color,
bg_color: Color,
border_color: Color,
) {
let area = ctx.area;
for y in 0..area.height {
for x in 0..area.width {
let mut cell = Cell::new(' ');
cell.bg = Some(bg_color);
ctx.set(x, y, cell);
}
}
self.draw_alert_border(ctx, border_color, bg_color);
let content_x: u16 = 2;
let content_width = area.width.saturating_sub(4);
let mut y: u16 = 1;
let icon_offset = if self.show_icon {
let icon = self.get_icon();
let mut icon_cell = Cell::new(icon);
icon_cell.fg = Some(accent_color);
icon_cell.bg = Some(bg_color);
ctx.set(content_x, y, icon_cell);
2
} else {
0
};
if let Some(ref title) = self.title {
let text_x = content_x + icon_offset;
let max_w = content_width.saturating_sub(icon_offset);
ctx.draw_text_clipped_bg_bold(text_x, y, title, Color::WHITE, bg_color, max_w);
y += 1;
ctx.draw_text_clipped_bg(text_x, y, &self.message, SECONDARY_TEXT, bg_color, max_w);
} else {
let text_x = content_x + icon_offset;
let max_w = content_width.saturating_sub(icon_offset);
ctx.draw_text_clipped_bg(text_x, y, &self.message, Color::WHITE, bg_color, max_w);
}
if self.dismissible {
let dismiss_x = area.width - 3;
let mut x_cell = Cell::new('×');
x_cell.fg = Some(LIGHT_GRAY);
x_cell.bg = Some(bg_color);
ctx.set(dismiss_x, 1, x_cell);
}
}
fn render_outlined(&self, ctx: &mut RenderContext, accent_color: Color, _border_color: Color) {
let text_fg = self.state.resolve_fg(ctx.style, Color::WHITE);
let area = ctx.area;
for y in 0..area.height {
let mut cell = Cell::new('┃');
cell.fg = Some(accent_color);
ctx.set(0, y, cell);
}
let content_x: u16 = 2;
let content_width = area.width.saturating_sub(3);
let mut y: u16 = 0;
let icon_offset = if self.show_icon {
let icon = self.get_icon();
let mut icon_cell = Cell::new(icon);
icon_cell.fg = Some(accent_color);
ctx.set(content_x, y, icon_cell);
2
} else {
0
};
if let Some(ref title) = self.title {
let title_x = content_x + icon_offset;
let max_w = content_width - icon_offset;
let mut dx: u16 = 0;
for ch in title.chars() {
let cw = char_width(ch) as u16;
if dx + cw > max_w {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(text_fg);
cell.modifier |= Modifier::BOLD;
ctx.set(title_x + dx, y, cell);
dx += cw;
}
y += 1;
let msg_x = content_x + icon_offset;
let mut dx: u16 = 0;
for ch in self.message.chars() {
let cw = char_width(ch) as u16;
if dx + cw > max_w {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(MUTED_TEXT);
ctx.set(msg_x + dx, y, cell);
dx += cw;
}
} else {
let msg_x = content_x + icon_offset;
let max_w = content_width - icon_offset;
let mut dx: u16 = 0;
for ch in self.message.chars() {
let cw = char_width(ch) as u16;
if dx + cw > max_w {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(text_fg);
ctx.set(msg_x + dx, y, cell);
dx += cw;
}
}
if self.dismissible {
let dismiss_x = area.width - 2;
let mut x_cell = Cell::new('×');
x_cell.fg = Some(LIGHT_GRAY);
ctx.set(dismiss_x, 0, x_cell);
}
}
fn render_minimal(&self, ctx: &mut RenderContext, accent_color: Color) {
let text_fg = self.state.resolve_fg(ctx.style, Color::WHITE);
let area = ctx.area;
let mut x: u16 = 0;
let y: u16 = 0;
if self.show_icon {
let icon = self.get_icon();
let mut icon_cell = Cell::new(icon);
icon_cell.fg = Some(accent_color);
ctx.set(x, y, icon_cell);
x += 2;
}
if let Some(ref title) = self.title {
let mut dx: u16 = 0;
for ch in title.chars() {
let cw = char_width(ch) as u16;
if x + dx + cw > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(accent_color);
cell.modifier |= Modifier::BOLD;
ctx.set(x + dx, y, cell);
dx += cw;
}
if area.height > 1 {
let msg_x: u16 = if self.show_icon { 2 } else { 0 };
let mut dx: u16 = 0;
for ch in self.message.chars() {
let cw = char_width(ch) as u16;
if msg_x + dx + cw > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(MUTED_TEXT);
ctx.set(msg_x + dx, y + 1, cell);
dx += cw;
}
}
} else {
let mut dx: u16 = 0;
for ch in self.message.chars() {
let cw = char_width(ch) as u16;
if x + dx + cw > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(text_fg);
ctx.set(x + dx, y, cell);
dx += cw;
}
}
if self.dismissible {
let dismiss_x = area.width - 1;
let mut x_cell = Cell::new('×');
x_cell.fg = Some(DISABLED_FG);
ctx.set(dismiss_x, y, x_cell);
}
}
fn draw_alert_border(&self, ctx: &mut RenderContext, border_color: Color, bg_color: Color) {
draw_border(
ctx.buffer,
ctx.area,
BorderType::Rounded,
Some(border_color),
Some(bg_color),
);
}
}
impl_styled_view!(Alert);
impl_widget_builders!(Alert);
pub fn alert(message: impl Into<String>) -> Alert {
Alert::new(message)
}
pub fn info_alert(message: impl Into<String>) -> Alert {
Alert::info(message)
}
pub fn success_alert(message: impl Into<String>) -> Alert {
Alert::success(message)
}
pub fn warning_alert(message: impl Into<String>) -> Alert {
Alert::warning(message)
}
pub fn error_alert(message: impl Into<String>) -> Alert {
Alert::error(message)
}