use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::utils::{char_width, display_width};
use crate::widget::theme::{LIGHT_GRAY, PLACEHOLDER_FG};
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 EmptyStateType {
#[default]
Empty,
NoResults,
Error,
NoPermission,
Offline,
FirstUse,
}
impl EmptyStateType {
pub fn icon(&self) -> char {
match self {
EmptyStateType::Empty => '📭',
EmptyStateType::NoResults => '🔍',
EmptyStateType::Error => '⚠',
EmptyStateType::NoPermission => '🔒',
EmptyStateType::Offline => '📡',
EmptyStateType::FirstUse => '🚀',
}
}
pub fn color(&self) -> Color {
match self {
EmptyStateType::Empty => PLACEHOLDER_FG,
EmptyStateType::NoResults => Color::rgb(100, 149, 237),
EmptyStateType::Error => Color::rgb(220, 80, 80),
EmptyStateType::NoPermission => Color::rgb(255, 165, 0),
EmptyStateType::Offline => PLACEHOLDER_FG,
EmptyStateType::FirstUse => Color::rgb(100, 200, 100),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum EmptyStateVariant {
#[default]
Full,
Compact,
Minimal,
}
pub struct EmptyState {
title: String,
description: Option<String>,
state_type: EmptyStateType,
variant: EmptyStateVariant,
show_icon: bool,
custom_icon: Option<char>,
action: Option<String>,
state: WidgetState,
props: WidgetProps,
}
impl EmptyState {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
description: None,
state_type: EmptyStateType::default(),
variant: EmptyStateVariant::default(),
show_icon: true,
custom_icon: None,
action: None,
state: WidgetState::new(),
props: WidgetProps::new(),
}
}
pub fn no_results(title: impl Into<String>) -> Self {
Self::new(title).state_type(EmptyStateType::NoResults)
}
pub fn error(title: impl Into<String>) -> Self {
Self::new(title).state_type(EmptyStateType::Error)
}
pub fn no_permission(title: impl Into<String>) -> Self {
Self::new(title).state_type(EmptyStateType::NoPermission)
}
pub fn offline(title: impl Into<String>) -> Self {
Self::new(title).state_type(EmptyStateType::Offline)
}
pub fn first_use(title: impl Into<String>) -> Self {
Self::new(title).state_type(EmptyStateType::FirstUse)
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn state_type(mut self, state_type: EmptyStateType) -> Self {
self.state_type = state_type;
self
}
pub fn variant(mut self, variant: EmptyStateVariant) -> 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 action(mut self, action: impl Into<String>) -> Self {
self.action = Some(action.into());
self
}
fn get_icon(&self) -> char {
self.custom_icon.unwrap_or_else(|| self.state_type.icon())
}
pub fn height(&self) -> u16 {
match self.variant {
EmptyStateVariant::Full => {
let mut h = 5; if self.description.is_some() {
h += 1;
}
if self.action.is_some() {
h += 2;
}
h
}
EmptyStateVariant::Compact => {
let mut h = 3;
if self.description.is_some() {
h += 1;
}
h
}
EmptyStateVariant::Minimal => 1,
}
}
}
impl Default for EmptyState {
fn default() -> Self {
Self::new("No items")
}
}
impl View for EmptyState {
crate::impl_view_meta!("EmptyState");
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width < 5 || area.height < 1 {
return;
}
match self.variant {
EmptyStateVariant::Full => self.render_full(ctx),
EmptyStateVariant::Compact => self.render_compact(ctx),
EmptyStateVariant::Minimal => self.render_minimal(ctx),
}
}
}
impl EmptyState {
fn render_full(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let accent = self.state_type.color();
let content_height = self.height();
let start_y = if area.height > content_height {
(area.height - content_height) / 2
} else {
0u16
};
let mut y = start_y;
if self.show_icon && y < area.height {
let icon = self.get_icon();
let icon_x = area.width / 2;
let mut cell = Cell::new(icon);
cell.fg = Some(accent);
ctx.set(icon_x, y, cell);
y += 2;
}
if y < area.height {
let title_len = display_width(&self.title) as u16;
let title_x = area.width.saturating_sub(title_len) / 2;
let mut dx: u16 = 0;
for ch in self.title.chars() {
let cw = char_width(ch) as u16;
if title_x + dx + cw > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(Color::WHITE);
cell.modifier |= Modifier::BOLD;
ctx.set(title_x + dx, y, cell);
dx += cw;
}
y += 1;
}
if let Some(ref desc) = self.description {
if y < area.height {
let desc_len = display_width(desc) as u16;
let desc_x = area.width.saturating_sub(desc_len) / 2;
let mut dx: u16 = 0;
for ch in desc.chars() {
let cw = char_width(ch) as u16;
if desc_x + dx + cw > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(LIGHT_GRAY);
ctx.set(desc_x + dx, y, cell);
dx += cw;
}
y += 2;
}
}
if let Some(ref action_text) = self.action {
if y < area.height {
let btn_text = format!("[ {} ]", action_text);
let btn_len = display_width(&btn_text) as u16;
let btn_x = area.width.saturating_sub(btn_len) / 2;
let mut dx: u16 = 0;
for ch in btn_text.chars() {
let cw = char_width(ch) as u16;
if btn_x + dx + cw > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(accent);
ctx.set(btn_x + dx, y, cell);
dx += cw;
}
}
}
}
fn render_compact(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let accent = self.state_type.color();
let mut y: u16 = 0;
let mut x: u16 = 0;
if self.show_icon {
let icon = self.get_icon();
let mut cell = Cell::new(icon);
cell.fg = Some(accent);
ctx.set(x, y, cell);
x += 2;
}
let mut dx: u16 = 0;
for ch in self.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(Color::WHITE);
cell.modifier |= Modifier::BOLD;
ctx.set(x + dx, y, cell);
dx += cw;
}
y += 1;
if let Some(ref desc) = self.description {
if y < area.height {
let desc_x: u16 = if self.show_icon { 2 } else { 0 };
let mut dx: u16 = 0;
for ch in desc.chars() {
let cw = char_width(ch) as u16;
if desc_x + dx + cw > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(LIGHT_GRAY);
ctx.set(desc_x + dx, y, cell);
dx += cw;
}
y += 1;
}
}
if let Some(ref action_text) = self.action {
if y < area.height {
let action_x: u16 = if self.show_icon { 2 } else { 0 };
let btn_text = format!("[{}]", action_text);
let mut dx: u16 = 0;
for ch in btn_text.chars() {
let cw = char_width(ch) as u16;
if action_x + dx + cw > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(accent);
ctx.set(action_x + dx, y, cell);
dx += cw;
}
}
}
}
fn render_minimal(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let accent = self.state_type.color();
let mut x: u16 = 0;
if self.show_icon {
let icon = self.get_icon();
let mut cell = Cell::new(icon);
cell.fg = Some(accent);
ctx.set(x, 0, cell);
x += 2;
}
let mut dx: u16 = 0;
for ch in self.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(LIGHT_GRAY);
ctx.set(x + dx, 0, cell);
dx += cw;
}
}
}
impl_styled_view!(EmptyState);
impl_widget_builders!(EmptyState);
pub fn empty_state(title: impl Into<String>) -> EmptyState {
EmptyState::new(title)
}
pub fn no_results(title: impl Into<String>) -> EmptyState {
EmptyState::no_results(title)
}
pub fn empty_error(title: impl Into<String>) -> EmptyState {
EmptyState::error(title)
}
pub fn first_use(title: impl Into<String>) -> EmptyState {
EmptyState::first_use(title)
}