use super::{Widget, WidgetBase, WidgetId, LayoutContext, PaintContext, EventContext};
use crate::css::{ClassList, WidgetState};
use crate::event::{Event, EventResult, MouseEventKind, MouseButton};
use crate::geometry::{Point, Rect, Size};
use crate::layout::{Constraints, LayoutResult};
use crate::render::Painter;
#[derive(Debug, Clone)]
pub struct TrayIcon {
pub id: String,
pub icon: String,
pub tooltip: Option<String>,
pub has_badge: bool,
pub badge_count: Option<u32>,
}
impl TrayIcon {
pub fn new(id: impl Into<String>, icon: impl Into<String>) -> Self {
Self {
id: id.into(),
icon: icon.into(),
tooltip: None,
has_badge: false,
badge_count: None,
}
}
pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
pub fn badge(mut self, count: Option<u32>) -> Self {
self.has_badge = count.is_some();
self.badge_count = count;
self
}
}
#[allow(clippy::type_complexity)]
pub struct SystemTray {
base: WidgetBase,
icons: Vec<TrayIcon>,
icon_size: f32,
icon_spacing: f32,
hovered_icon: Option<String>,
on_click: Option<Box<dyn Fn(&str) + Send + Sync>>,
on_right_click: Option<Box<dyn Fn(&str) + Send + Sync>>,
}
impl SystemTray {
pub fn new() -> Self {
Self {
base: WidgetBase::new().with_class("system-tray"),
icons: Vec::new(),
icon_size: 20.0,
icon_spacing: 8.0,
hovered_icon: None,
on_click: None,
on_right_click: None,
}
}
pub fn icon(mut self, icon: TrayIcon) -> Self {
self.icons.push(icon);
self
}
pub fn icons(mut self, icons: Vec<TrayIcon>) -> Self {
self.icons = icons;
self
}
pub fn icon_size(mut self, size: f32) -> Self {
self.icon_size = size;
self
}
pub fn icon_spacing(mut self, spacing: f32) -> Self {
self.icon_spacing = spacing;
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.on_click = Some(Box::new(handler));
self
}
pub fn on_right_click<F>(mut self, handler: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.on_right_click = Some(Box::new(handler));
self
}
pub fn class(mut self, class: &str) -> Self {
self.base.classes.add(class);
self
}
pub fn update_icon<F>(&mut self, id: &str, f: F)
where
F: FnOnce(&mut TrayIcon),
{
if let Some(icon) = self.icons.iter_mut().find(|i| i.id == id) {
f(icon);
}
}
fn get_icon_rect(&self, index: usize) -> Rect {
let cell_size = self.icon_size + self.icon_spacing;
let x = self.base.bounds.x() + (index as f32) * cell_size;
let y = self.base.bounds.y() + (self.base.bounds.height() - self.icon_size) / 2.0;
Rect::new(x, y, self.icon_size, self.icon_size)
}
fn icon_at_point(&self, point: Point) -> Option<usize> {
for (i, _) in self.icons.iter().enumerate() {
let rect = self.get_icon_rect(i);
let hit_rect = Rect::new(
rect.x() - self.icon_spacing / 2.0,
rect.y() - 4.0,
rect.width() + self.icon_spacing,
rect.height() + 8.0,
);
if hit_rect.contains(point) {
return Some(i);
}
}
None
}
}
impl Default for SystemTray {
fn default() -> Self {
Self::new()
}
}
impl Widget for SystemTray {
fn id(&self) -> WidgetId {
self.base.id
}
fn type_name(&self) -> &'static str {
"system-tray"
}
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 width = self.icons.len() as f32 * (self.icon_size + self.icon_spacing) - self.icon_spacing;
Size::new(width.max(0.0), self.icon_size + 8.0)
}
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;
for (i, icon) in self.icons.iter().enumerate() {
let icon_rect = self.get_icon_rect(i);
let is_hovered = self.hovered_icon.as_ref() == Some(&icon.id);
if is_hovered {
let hover_rect = Rect::new(
icon_rect.x() - 4.0,
icon_rect.y() - 4.0,
icon_rect.width() + 8.0,
icon_rect.height() + 8.0,
);
painter.fill_rect(hover_rect, theme.colors.accent.with_alpha(0.2));
}
painter.draw_text(
&icon.icon,
Point::new(icon_rect.x(), icon_rect.y() + self.icon_size * 0.85),
theme.colors.foreground,
self.icon_size,
);
if icon.has_badge {
let badge_size = 12.0;
let badge_x = icon_rect.x() + icon_rect.width() - badge_size / 2.0;
let badge_y = icon_rect.y() - badge_size / 4.0;
painter.fill_rect(
Rect::new(badge_x, badge_y, badge_size, badge_size),
theme.colors.destructive,
);
if let Some(count) = icon.badge_count {
let count_str = if count > 9 { "9+".to_string() } else { count.to_string() };
painter.draw_text(
&count_str,
Point::new(badge_x + 2.0, badge_y + badge_size * 0.8),
theme.colors.destructive_foreground,
8.0,
);
}
}
}
}
fn handle_event(&mut self, event: &Event, ctx: &mut EventContext) -> EventResult {
if let Event::Mouse(mouse) = event {
match mouse.kind {
MouseEventKind::Move => {
let new_hovered = self.icon_at_point(mouse.position)
.map(|i| self.icons[i].id.clone());
if new_hovered != self.hovered_icon {
self.hovered_icon = new_hovered;
ctx.request_redraw();
}
}
MouseEventKind::Leave => {
if self.hovered_icon.is_some() {
self.hovered_icon = None;
ctx.request_redraw();
}
}
MouseEventKind::Up if mouse.button == Some(MouseButton::Left) => {
if let Some(index) = self.icon_at_point(mouse.position) {
let id = self.icons[index].id.clone();
if let Some(handler) = &self.on_click {
handler(&id);
}
return EventResult::Handled;
}
}
MouseEventKind::Up if mouse.button == Some(MouseButton::Right) => {
if let Some(index) = self.icon_at_point(mouse.position) {
let id = self.icons[index].id.clone();
if let Some(handler) = &self.on_right_click {
handler(&id);
}
return EventResult::Handled;
}
}
_ => {}
}
}
EventResult::Ignored
}
fn bounds(&self) -> Rect {
self.base.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.base.bounds = bounds;
}
}