use super::{Widget, WidgetBase, WidgetId, LayoutContext, PaintContext, EventContext};
use crate::css::{ClassList, WidgetState};
use crate::event::{Event, EventResult, MouseEventKind, MouseButton};
use crate::geometry::{BorderRadius, Color, Point, Rect, Size};
use crate::layout::{Constraints, LayoutResult};
use crate::render::Painter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VpnConnectionStatus {
#[default]
Disconnected,
Connecting,
Authenticating,
Connected,
Disconnecting,
Error,
}
impl VpnConnectionStatus {
pub fn label(&self) -> &'static str {
match self {
VpnConnectionStatus::Disconnected => "Disconnected",
VpnConnectionStatus::Connecting => "Connecting...",
VpnConnectionStatus::Authenticating => "Authenticating...",
VpnConnectionStatus::Connected => "Connected",
VpnConnectionStatus::Disconnecting => "Disconnecting...",
VpnConnectionStatus::Error => "Error",
}
}
pub fn color(&self) -> Color {
match self {
VpnConnectionStatus::Disconnected => Color::from_rgb(128, 128, 128),
VpnConnectionStatus::Connecting | VpnConnectionStatus::Authenticating => {
Color::from_rgb(255, 193, 7) }
VpnConnectionStatus::Connected => Color::from_rgb(34, 197, 94), VpnConnectionStatus::Disconnecting => Color::from_rgb(255, 193, 7),
VpnConnectionStatus::Error => Color::from_rgb(239, 68, 68), }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VpnStatusSize {
Compact,
Small,
#[default]
Medium,
Large,
}
impl VpnStatusSize {
fn indicator_size(&self) -> f32 {
match self {
VpnStatusSize::Compact => 12.0,
VpnStatusSize::Small => 16.0,
VpnStatusSize::Medium => 20.0,
VpnStatusSize::Large => 28.0,
}
}
fn font_size(&self) -> f32 {
match self {
VpnStatusSize::Compact => 0.0,
VpnStatusSize::Small => 12.0,
VpnStatusSize::Medium => 14.0,
VpnStatusSize::Large => 18.0,
}
}
}
pub struct VpnStatus {
base: WidgetBase,
status: VpnConnectionStatus,
size: VpnStatusSize,
show_label: bool,
server_name: Option<String>,
pulse: bool,
on_click: Option<Box<dyn Fn() + Send + Sync>>,
}
impl VpnStatus {
pub fn new() -> Self {
Self {
base: WidgetBase::new().with_class("vpn-status"),
status: VpnConnectionStatus::default(),
size: VpnStatusSize::default(),
show_label: true,
server_name: None,
pulse: true,
on_click: None,
}
}
pub fn status(mut self, status: VpnConnectionStatus) -> Self {
self.status = status;
self
}
pub fn size(mut self, size: VpnStatusSize) -> Self {
self.size = size;
self
}
pub fn show_label(mut self, show: bool) -> Self {
self.show_label = show;
self
}
pub fn server_name(mut self, name: impl Into<String>) -> Self {
self.server_name = Some(name.into());
self
}
pub fn pulse(mut self, pulse: bool) -> Self {
self.pulse = pulse;
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_click = Some(Box::new(handler));
self
}
pub fn class(mut self, class: &str) -> Self {
self.base.classes.add(class);
self
}
pub fn get_status(&self) -> VpnConnectionStatus {
self.status
}
pub fn set_status(&mut self, status: VpnConnectionStatus) {
self.status = status;
}
fn is_transitional(&self) -> bool {
matches!(
self.status,
VpnConnectionStatus::Connecting
| VpnConnectionStatus::Authenticating
| VpnConnectionStatus::Disconnecting
)
}
}
impl Default for VpnStatus {
fn default() -> Self {
Self::new()
}
}
impl Widget for VpnStatus {
fn id(&self) -> WidgetId {
self.base.id
}
fn type_name(&self) -> &'static str {
"vpn-status"
}
fn element_id(&self) -> Option<&str> {
self.base.element_id.as_deref()
}
fn classes(&self) -> &ClassList {
&self.base.classes
}
fn state(&self) -> WidgetState {
self.base.state
}
fn intrinsic_size(&self, _ctx: &LayoutContext) -> Size {
let indicator_size = self.size.indicator_size();
let font_size = self.size.font_size();
if self.size == VpnStatusSize::Compact {
return Size::new(indicator_size, indicator_size);
}
let label_width = if self.show_label {
let status_label_len = self.status.label().len() as f32 * font_size * 0.6;
let server_label_len = self
.server_name
.as_ref()
.map(|n| n.len() as f32 * font_size * 0.5)
.unwrap_or(0.0);
status_label_len.max(server_label_len) + 16.0
} else {
0.0
};
let height = match self.size {
VpnStatusSize::Large if self.server_name.is_some() => indicator_size + font_size + 8.0,
_ => indicator_size.max(font_size * 1.5),
};
Size::new(indicator_size + label_width, height)
}
fn layout(&mut self, constraints: Constraints, ctx: &LayoutContext) -> LayoutResult {
let size = constraints.constrain(self.intrinsic_size(ctx));
self.base.bounds.size = size;
LayoutResult::new(size)
}
fn paint(&self, painter: &mut Painter, rect: Rect, ctx: &PaintContext) {
let theme = ctx.style_ctx.theme;
let indicator_size = self.size.indicator_size();
let status_color = self.status.color();
if self.pulse && self.is_transitional() {
let glow_size = indicator_size * 1.6;
let glow_rect = Rect::new(
rect.x() + (indicator_size - glow_size) / 2.0,
rect.y() + (rect.height() - glow_size) / 2.0,
glow_size,
glow_size,
);
painter.fill_rounded_rect(
glow_rect,
status_color.with_alpha(0.3),
BorderRadius::all(glow_size / 2.0),
);
}
let indicator_rect = Rect::new(
rect.x(),
rect.y() + (rect.height() - indicator_size) / 2.0,
indicator_size,
indicator_size,
);
painter.fill_rounded_rect(
indicator_rect,
status_color,
BorderRadius::all(indicator_size / 2.0),
);
let highlight_size = indicator_size * 0.6;
let highlight_rect = Rect::new(
indicator_rect.x() + (indicator_size - highlight_size) / 2.0,
indicator_rect.y() + (indicator_size - highlight_size) / 2.0,
highlight_size,
highlight_size,
);
painter.fill_rounded_rect(
highlight_rect,
status_color.lighten(20.0),
BorderRadius::all(highlight_size / 2.0),
);
if self.show_label && self.size != VpnStatusSize::Compact {
let font_size = self.size.font_size();
let text_x = rect.x() + indicator_size + 12.0;
if self.size == VpnStatusSize::Large && self.server_name.is_some() {
let status_y = rect.y() + font_size;
painter.draw_text(
self.status.label(),
Point::new(text_x, status_y),
theme.colors.foreground,
font_size,
);
if let Some(server) = &self.server_name {
let server_y = status_y + font_size + 4.0;
painter.draw_text(
server,
Point::new(text_x, server_y),
theme.colors.muted_foreground,
font_size * 0.85,
);
}
} else {
let text_y = rect.y() + (rect.height() + font_size * 0.8) / 2.0;
painter.draw_text(
self.status.label(),
Point::new(text_x, text_y),
theme.colors.foreground,
font_size,
);
}
}
}
fn handle_event(&mut self, event: &Event, ctx: &mut EventContext) -> EventResult {
if let Event::Mouse(mouse) = event {
let in_bounds = self.bounds().contains(mouse.position);
match mouse.kind {
MouseEventKind::Move | MouseEventKind::Enter => {
if in_bounds && !self.base.state.hovered {
self.base.state.hovered = true;
ctx.request_redraw();
} else if !in_bounds && self.base.state.hovered {
self.base.state.hovered = false;
ctx.request_redraw();
}
}
MouseEventKind::Leave => {
if self.base.state.hovered {
self.base.state.hovered = false;
ctx.request_redraw();
}
}
MouseEventKind::Up if mouse.button == Some(MouseButton::Left) && in_bounds => {
if let Some(handler) = &self.on_click {
handler();
}
ctx.request_redraw();
return EventResult::Handled;
}
_ => {}
}
}
EventResult::Ignored
}
fn bounds(&self) -> Rect {
self.base.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.base.bounds = bounds;
}
}