use crate::render::Cell;
use crate::style::Color;
use crate::widget::theme::{DARK_BG, SECONDARY_TEXT};
use crate::widget::traits::{RenderContext, View, WidgetProps, WidgetState};
use crate::{impl_styled_view, impl_widget_builders};
use unicode_width::UnicodeWidthChar;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Status {
#[default]
Online,
Offline,
Busy,
Away,
Unknown,
Error,
Custom(Color),
}
impl Status {
pub fn color(&self) -> Color {
match self {
Status::Online => Color::rgb(34, 197, 94), Status::Offline => Color::rgb(107, 114, 128), Status::Busy => Color::rgb(239, 68, 68), Status::Away => Color::rgb(234, 179, 8), Status::Unknown => Color::rgb(156, 163, 175), Status::Error => Color::rgb(220, 38, 38), Status::Custom(color) => *color,
}
}
pub fn label(&self) -> &'static str {
match self {
Status::Online => "Online",
Status::Offline => "Offline",
Status::Busy => "Busy",
Status::Away => "Away",
Status::Unknown => "Unknown",
Status::Error => "Error",
Status::Custom(_) => "Custom",
}
}
pub fn icon(&self) -> char {
match self {
Status::Online => '●',
Status::Offline => '○',
Status::Busy => '⊘',
Status::Away => '◐',
Status::Unknown => '?',
Status::Error => '!',
Status::Custom(_) => '●',
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StatusSize {
Small,
#[default]
Medium,
Large,
}
impl StatusSize {
pub fn dot(&self) -> char {
match self {
StatusSize::Small => '•',
StatusSize::Medium => '●',
StatusSize::Large => '⬤',
}
}
pub fn width(&self) -> u16 {
match self {
StatusSize::Small => 1,
StatusSize::Medium => 1,
StatusSize::Large => 2,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StatusStyle {
#[default]
Dot,
DotWithLabel,
LabelOnly,
Badge,
}
#[derive(Clone)]
pub struct StatusIndicator {
status: Status,
size: StatusSize,
style: StatusStyle,
custom_label: Option<String>,
pulsing: bool,
frame: usize,
state: WidgetState,
props: WidgetProps,
}
impl StatusIndicator {
pub fn new(status: Status) -> Self {
Self {
status,
size: StatusSize::default(),
style: StatusStyle::default(),
custom_label: None,
pulsing: false,
frame: 0,
state: WidgetState::new(),
props: WidgetProps::new(),
}
}
pub fn online() -> Self {
Self::new(Status::Online)
}
pub fn offline() -> Self {
Self::new(Status::Offline)
}
pub fn busy() -> Self {
Self::new(Status::Busy)
}
pub fn away() -> Self {
Self::new(Status::Away)
}
pub fn unknown() -> Self {
Self::new(Status::Unknown)
}
pub fn error() -> Self {
Self::new(Status::Error)
}
pub fn custom(color: Color) -> Self {
Self::new(Status::Custom(color))
}
pub fn status(mut self, status: Status) -> Self {
self.status = status;
self
}
pub fn size(mut self, size: StatusSize) -> Self {
self.size = size;
self
}
pub fn indicator_style(mut self, style: StatusStyle) -> Self {
self.style = style;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.custom_label = Some(label.into());
self
}
pub fn pulsing(mut self, pulsing: bool) -> Self {
self.pulsing = pulsing;
self
}
pub fn tick(&mut self) {
self.frame = self.frame.wrapping_add(1);
}
pub fn get_status(&self) -> Status {
self.status
}
pub fn set_status(&mut self, status: Status) {
self.status = status;
}
fn get_label(&self) -> &str {
self.custom_label
.as_deref()
.unwrap_or_else(|| self.status.label())
}
fn is_visible(&self) -> bool {
if !self.pulsing {
return true;
}
(self.frame % 8) < 6
}
pub fn width(&self) -> u16 {
match self.style {
StatusStyle::Dot => self.size.width(),
StatusStyle::DotWithLabel => {
let label_len = crate::utils::display_width(self.get_label()) as u16;
self.size.width() + 1 + label_len }
StatusStyle::LabelOnly => crate::utils::display_width(self.get_label()) as u16,
StatusStyle::Badge => {
let label_len = crate::utils::display_width(self.get_label()) as u16;
label_len + 4 }
}
}
}
impl Default for StatusIndicator {
fn default() -> Self {
Self::new(Status::Online)
}
}
impl View for StatusIndicator {
crate::impl_view_meta!("StatusIndicator");
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width < 1 || area.height < 1 {
return;
}
let color = self.status.color();
let visible = self.is_visible();
match self.style {
StatusStyle::Dot => {
self.render_dot(ctx, color, visible);
}
StatusStyle::DotWithLabel => {
self.render_dot_with_label(ctx, color, visible);
}
StatusStyle::LabelOnly => {
self.render_label_only(ctx, color);
}
StatusStyle::Badge => {
self.render_badge(ctx, color, visible);
}
}
}
}
impl StatusIndicator {
fn render_dot(&self, ctx: &mut RenderContext, color: Color, visible: bool) {
let area = ctx.area;
let dot = if visible { self.size.dot() } else { ' ' };
let mut cell = Cell::new(dot);
cell.fg = Some(color);
ctx.set(0, 0, cell);
if self.size == StatusSize::Large && area.width > 1 {
let mut cell2 = Cell::new(' ');
cell2.bg = Some(color);
ctx.set(1, 0, cell2);
}
}
fn render_dot_with_label(&self, ctx: &mut RenderContext, color: Color, visible: bool) {
let area = ctx.area;
let dot = if visible { self.size.dot() } else { ' ' };
let mut dot_cell = Cell::new(dot);
dot_cell.fg = Some(color);
ctx.set(0, 0, dot_cell);
let label = self.get_label();
let label_start = self.size.width() + 1;
let max_label_width = area.width.saturating_sub(self.size.width() + 1);
let mut offset = 0u16;
for ch in label.chars() {
let char_width = ch.width().unwrap_or(0) as u16;
if char_width == 0 {
continue;
}
if offset + char_width > max_label_width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(SECONDARY_TEXT);
ctx.set(label_start + offset, 0, cell);
for i in 1..char_width {
ctx.set(label_start + offset + i, 0, Cell::continuation());
}
offset += char_width;
}
}
fn render_label_only(&self, ctx: &mut RenderContext, color: Color) {
let area = ctx.area;
let label = self.get_label();
let mut offset = 0u16;
for ch in label.chars() {
let char_width = ch.width().unwrap_or(0) as u16;
if char_width == 0 {
continue;
}
if offset + char_width > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(color);
ctx.set(offset, 0, cell);
for i in 1..char_width {
ctx.set(offset + i, 0, Cell::continuation());
}
offset += char_width;
}
}
fn render_badge(&self, ctx: &mut RenderContext, color: Color, visible: bool) {
let area = ctx.area;
let label = self.get_label();
let bg_color = DARK_BG;
let total_width = self.width().min(area.width);
for i in 0..total_width {
let mut cell = Cell::new(' ');
cell.bg = Some(bg_color);
ctx.set(i, 0, cell);
}
let dot = if visible { '●' } else { ' ' };
let mut dot_cell = Cell::new(dot);
dot_cell.fg = Some(color);
dot_cell.bg = Some(bg_color);
ctx.set(1, 0, dot_cell);
let label_start: u16 = 3;
let max_label_width = total_width.saturating_sub(4);
let mut offset = 0u16;
for ch in label.chars() {
let char_width = ch.width().unwrap_or(0) as u16;
if char_width == 0 {
continue;
}
if offset + char_width > max_label_width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(Color::WHITE);
cell.bg = Some(bg_color);
ctx.set(label_start + offset, 0, cell);
for i in 1..char_width {
let mut cont = Cell::continuation();
cont.bg = Some(bg_color);
ctx.set(label_start + offset + i, 0, cont);
}
offset += char_width;
}
}
}
impl_styled_view!(StatusIndicator);
impl_widget_builders!(StatusIndicator);
pub fn status_indicator(status: Status) -> StatusIndicator {
StatusIndicator::new(status)
}
pub fn online() -> StatusIndicator {
StatusIndicator::online()
}
pub fn offline() -> StatusIndicator {
StatusIndicator::offline()
}
pub fn busy_indicator() -> StatusIndicator {
StatusIndicator::busy()
}
pub fn away_indicator() -> StatusIndicator {
StatusIndicator::away()
}