use super::{Widget, WidgetBase, WidgetId, LayoutContext, PaintContext, EventContext};
use crate::css::{ClassList, WidgetState};
use crate::event::{Event, EventResult, MouseEventKind, MouseButton};
use crate::geometry::{Color, Point, Rect, Size};
use crate::layout::{Constraints, LayoutResult};
use crate::render::Painter;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WallpaperMode {
#[default]
Fill,
Fit,
Stretch,
Tile,
Center,
Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GradientDirection {
#[default]
ToBottom,
ToTop,
ToRight,
ToLeft,
ToBottomRight,
ToBottomLeft,
ToTopRight,
ToTopLeft,
Radial,
}
#[derive(Debug, Clone)]
pub enum Wallpaper {
Color(Color),
Image {
path: PathBuf,
mode: WallpaperMode,
fallback: Color,
},
Gradient {
start: Color,
end: Color,
direction: GradientDirection,
},
ImageWithOverlay {
path: PathBuf,
mode: WallpaperMode,
overlay: Color,
fallback: Color,
},
}
impl Default for Wallpaper {
fn default() -> Self {
Wallpaper::Color(Color::rgb(0.1, 0.1, 0.18))
}
}
impl Wallpaper {
pub fn color(color: Color) -> Self {
Wallpaper::Color(color)
}
pub fn image(path: impl Into<PathBuf>) -> Self {
Wallpaper::Image {
path: path.into(),
mode: WallpaperMode::default(),
fallback: Color::rgb(0.1, 0.1, 0.18),
}
}
pub fn image_with_mode(path: impl Into<PathBuf>, mode: WallpaperMode) -> Self {
Wallpaper::Image {
path: path.into(),
mode,
fallback: Color::rgb(0.1, 0.1, 0.18),
}
}
pub fn gradient(start: Color, end: Color) -> Self {
Wallpaper::Gradient {
start,
end,
direction: GradientDirection::default(),
}
}
pub fn gradient_with_direction(start: Color, end: Color, direction: GradientDirection) -> Self {
Wallpaper::Gradient {
start,
end,
direction,
}
}
pub fn image_with_overlay(path: impl Into<PathBuf>, overlay: Color) -> Self {
Wallpaper::ImageWithOverlay {
path: path.into(),
mode: WallpaperMode::Fill,
overlay,
fallback: Color::rgb(0.1, 0.1, 0.18),
}
}
pub fn with_mode(self, mode: WallpaperMode) -> Self {
match self {
Wallpaper::Image { path, fallback, .. } => Wallpaper::Image { path, mode, fallback },
Wallpaper::ImageWithOverlay { path, overlay, fallback, .. } => {
Wallpaper::ImageWithOverlay { path, mode, overlay, fallback }
}
other => other,
}
}
pub fn with_fallback(self, fallback: Color) -> Self {
match self {
Wallpaper::Image { path, mode, .. } => Wallpaper::Image { path, mode, fallback },
Wallpaper::ImageWithOverlay { path, mode, overlay, .. } => {
Wallpaper::ImageWithOverlay { path, mode, overlay, fallback }
}
other => other,
}
}
pub fn with_direction(self, direction: GradientDirection) -> Self {
match self {
Wallpaper::Gradient { start, end, .. } => Wallpaper::Gradient { start, end, direction },
other => other,
}
}
}
#[derive(Debug, Clone)]
pub struct DesktopIcon {
pub id: String,
pub name: String,
pub icon: String,
pub position: (usize, usize),
pub selected: bool,
}
impl DesktopIcon {
pub fn new(id: impl Into<String>, name: impl Into<String>, icon: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
icon: icon.into(),
position: (0, 0),
selected: false,
}
}
pub fn at(mut self, col: usize, row: usize) -> Self {
self.position = (col, row);
self
}
}
#[allow(clippy::type_complexity)]
pub struct Desktop {
base: WidgetBase,
wallpaper: Wallpaper,
icons: Vec<DesktopIcon>,
icon_size: f32,
cell_size: f32,
grid_padding: f32,
hovered_icon: Option<String>,
last_click_time: Option<std::time::Instant>,
last_click_id: Option<String>,
on_icon_click: Option<Box<dyn Fn(&str) + Send + Sync>>,
on_icon_double_click: Option<Box<dyn Fn(&str) + Send + Sync>>,
on_right_click: Option<Box<dyn Fn(Point) + Send + Sync>>,
on_wallpaper_change: Option<Box<dyn Fn(&Wallpaper) + Send + Sync>>,
}
impl Desktop {
pub fn new() -> Self {
Self {
base: WidgetBase::new().with_class("desktop"),
wallpaper: Wallpaper::default(),
icons: Vec::new(),
icon_size: 48.0,
cell_size: 80.0,
grid_padding: 16.0,
hovered_icon: None,
last_click_time: None,
last_click_id: None,
on_icon_click: None,
on_icon_double_click: None,
on_right_click: None,
on_wallpaper_change: None,
}
}
pub fn background(mut self, wallpaper: Wallpaper) -> Self {
self.wallpaper = wallpaper;
self
}
pub fn wallpaper_color(mut self, color: Color) -> Self {
self.wallpaper = Wallpaper::Color(color);
self
}
pub fn wallpaper_image(mut self, path: impl Into<PathBuf>) -> Self {
self.wallpaper = Wallpaper::image(path);
self
}
pub fn wallpaper_image_with_mode(mut self, path: impl Into<PathBuf>, mode: WallpaperMode) -> Self {
self.wallpaper = Wallpaper::image_with_mode(path, mode);
self
}
pub fn wallpaper_gradient(mut self, start: Color, end: Color, direction: GradientDirection) -> Self {
self.wallpaper = Wallpaper::gradient_with_direction(start, end, direction);
self
}
pub fn icon(mut self, icon: DesktopIcon) -> Self {
self.icons.push(icon);
self
}
pub fn icons(mut self, icons: Vec<DesktopIcon>) -> Self {
self.icons = icons;
self
}
pub fn icon_size(mut self, size: f32) -> Self {
self.icon_size = size;
self
}
pub fn cell_size(mut self, size: f32) -> Self {
self.cell_size = size;
self
}
pub fn on_icon_click<F>(mut self, handler: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.on_icon_click = Some(Box::new(handler));
self
}
pub fn on_icon_double_click<F>(mut self, handler: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.on_icon_double_click = Some(Box::new(handler));
self
}
pub fn on_right_click<F>(mut self, handler: F) -> Self
where
F: Fn(Point) + Send + Sync + 'static,
{
self.on_right_click = Some(Box::new(handler));
self
}
pub fn on_wallpaper_change<F>(mut self, handler: F) -> Self
where
F: Fn(&Wallpaper) + Send + Sync + 'static,
{
self.on_wallpaper_change = Some(Box::new(handler));
self
}
pub fn class(mut self, class: &str) -> Self {
self.base.classes.add(class);
self
}
pub fn get_wallpaper(&self) -> &Wallpaper {
&self.wallpaper
}
pub fn set_wallpaper(&mut self, wallpaper: Wallpaper) {
self.wallpaper = wallpaper;
if let Some(handler) = &self.on_wallpaper_change {
handler(&self.wallpaper);
}
}
fn get_icon_rect(&self, col: usize, row: usize) -> Rect {
let x = self.base.bounds.x() + self.grid_padding + (col as f32) * self.cell_size;
let y = self.base.bounds.y() + self.grid_padding + (row as f32) * self.cell_size;
Rect::new(x, y, self.cell_size, self.cell_size)
}
fn icon_at_point(&self, point: Point) -> Option<&DesktopIcon> {
for icon in &self.icons {
let rect = self.get_icon_rect(icon.position.0, icon.position.1);
if rect.contains(point) {
return Some(icon);
}
}
None
}
pub fn select_icon(&mut self, id: &str) {
for icon in &mut self.icons {
icon.selected = icon.id == id;
}
}
pub fn clear_selection(&mut self) {
for icon in &mut self.icons {
icon.selected = false;
}
}
fn paint_wallpaper(&self, painter: &mut Painter, rect: Rect) {
match &self.wallpaper {
Wallpaper::Color(color) => {
painter.fill_rect(rect, *color);
}
Wallpaper::Image { path: _, mode: _, fallback } => {
painter.fill_rect(rect, *fallback);
self.draw_image_placeholder(painter, rect);
}
Wallpaper::Gradient { start, end, direction } => {
self.paint_gradient(painter, rect, *start, *end, *direction);
}
Wallpaper::ImageWithOverlay { path: _, mode: _, overlay, fallback } => {
painter.fill_rect(rect, *fallback);
self.draw_image_placeholder(painter, rect);
painter.fill_rect(rect, *overlay);
}
}
}
fn draw_image_placeholder(&self, painter: &mut Painter, rect: Rect) {
let grid_size: f32 = 40.0;
let line_color = Color::WHITE.with_alpha(0.03);
let mut x = rect.x();
while x < rect.x() + rect.width() {
painter.fill_rect(Rect::new(x, rect.y(), 1.0, rect.height()), line_color);
x += grid_size;
}
let mut y = rect.y();
while y < rect.y() + rect.height() {
painter.fill_rect(Rect::new(rect.x(), y, rect.width(), 1.0), line_color);
y += grid_size;
}
}
fn paint_gradient(&self, painter: &mut Painter, rect: Rect, start: Color, end: Color, direction: GradientDirection) {
let steps = 64;
match direction {
GradientDirection::ToBottom | GradientDirection::ToTop => {
let (from, to) = if direction == GradientDirection::ToBottom {
(start, end)
} else {
(end, start)
};
let step_height = rect.height() / steps as f32;
for i in 0..steps {
let t = i as f32 / (steps - 1) as f32;
let color = Self::lerp_color(from, to, t);
let y = rect.y() + (i as f32) * step_height;
painter.fill_rect(Rect::new(rect.x(), y, rect.width(), step_height + 1.0), color);
}
}
GradientDirection::ToRight | GradientDirection::ToLeft => {
let (from, to) = if direction == GradientDirection::ToRight {
(start, end)
} else {
(end, start)
};
let step_width = rect.width() / steps as f32;
for i in 0..steps {
let t = i as f32 / (steps - 1) as f32;
let color = Self::lerp_color(from, to, t);
let x = rect.x() + (i as f32) * step_width;
painter.fill_rect(Rect::new(x, rect.y(), step_width + 1.0, rect.height()), color);
}
}
GradientDirection::ToBottomRight | GradientDirection::ToTopLeft => {
let (from, to) = if direction == GradientDirection::ToBottomRight {
(start, end)
} else {
(end, start)
};
for i in 0..steps {
let t = i as f32 / (steps - 1) as f32;
let color = Self::lerp_color(from, to, t);
let offset = t * (rect.width() + rect.height());
for j in 0..10 {
let y = rect.y() + offset - rect.width() + (j as f32);
if y >= rect.y() && y < rect.y() + rect.height() {
painter.fill_rect(Rect::new(rect.x(), y, rect.width(), 1.0), color);
}
}
}
}
GradientDirection::ToBottomLeft | GradientDirection::ToTopRight => {
let (from, to) = if direction == GradientDirection::ToBottomLeft {
(start, end)
} else {
(end, start)
};
for i in 0..steps {
let t = i as f32 / (steps - 1) as f32;
let color = Self::lerp_color(from, to, t);
let offset = t * (rect.width() + rect.height());
for j in 0..10 {
let y = rect.y() + offset - rect.width() + (j as f32);
if y >= rect.y() && y < rect.y() + rect.height() {
painter.fill_rect(Rect::new(rect.x(), y, rect.width(), 1.0), color);
}
}
}
}
GradientDirection::Radial => {
let center_x = rect.x() + rect.width() / 2.0;
let center_y = rect.y() + rect.height() / 2.0;
let max_radius = (rect.width().powi(2) + rect.height().powi(2)).sqrt() / 2.0;
for i in (0..steps).rev() {
let t = i as f32 / (steps - 1) as f32;
let color = Self::lerp_color(start, end, t);
let radius = max_radius * t;
let size = radius * 2.0;
painter.fill_rect(
Rect::new(center_x - radius, center_y - radius, size, size),
color,
);
}
}
}
}
fn lerp_color(from: Color, to: Color, t: f32) -> Color {
Color::rgba(
from.r + (to.r - from.r) * t,
from.g + (to.g - from.g) * t,
from.b + (to.b - from.b) * t,
from.a + (to.a - from.a) * t,
)
}
}
impl Default for Desktop {
fn default() -> Self {
Self::new()
}
}
impl Widget for Desktop {
fn id(&self) -> WidgetId {
self.base.id
}
fn type_name(&self) -> &'static str {
"desktop"
}
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 {
Size::new(f32::MAX, f32::MAX)
}
fn layout(&mut self, constraints: Constraints, _ctx: &LayoutContext) -> LayoutResult {
let size = Size::new(constraints.max_width, constraints.max_height);
self.base.bounds.size = size;
LayoutResult::new(size)
}
fn paint(&self, painter: &mut Painter, rect: Rect, ctx: &PaintContext) {
let theme = ctx.style_ctx.theme;
self.paint_wallpaper(painter, rect);
for icon in &self.icons {
let cell_rect = self.get_icon_rect(icon.position.0, icon.position.1);
if icon.selected {
painter.fill_rect(
cell_rect,
theme.colors.accent.with_alpha(0.3),
);
} else if self.hovered_icon.as_ref() == Some(&icon.id) {
painter.fill_rect(
cell_rect,
theme.colors.accent.with_alpha(0.15),
);
}
let icon_x = cell_rect.x() + (cell_rect.width() - self.icon_size) / 2.0;
let icon_y = cell_rect.y() + 8.0;
painter.draw_text(
&icon.icon,
Point::new(icon_x, icon_y + self.icon_size * 0.8),
Color::WHITE,
self.icon_size,
);
let label_y = icon_y + self.icon_size + 8.0;
let font_size = 11.0;
let label_x = cell_rect.x() + (cell_rect.width() - icon.name.len() as f32 * font_size * 0.5) / 2.0;
painter.draw_text(
&icon.name,
Point::new(label_x, label_y + font_size),
Color::WHITE,
font_size,
);
}
}
fn handle_event(&mut self, event: &Event, ctx: &mut EventContext) -> EventResult {
if let Event::Mouse(mouse) = event {
match mouse.kind {
MouseEventKind::Move => {
let icon = self.icon_at_point(mouse.position);
let new_hovered = icon.map(|i| i.id.clone());
if new_hovered != self.hovered_icon {
self.hovered_icon = new_hovered;
ctx.request_redraw();
}
}
MouseEventKind::Down if mouse.button == Some(MouseButton::Left) => {
if let Some(icon) = self.icon_at_point(mouse.position) {
let icon_id = icon.id.clone();
let now = std::time::Instant::now();
let is_double_click = if let (Some(last_time), Some(last_id)) =
(&self.last_click_time, &self.last_click_id)
{
now.duration_since(*last_time).as_millis() < 500 && last_id == &icon_id
} else {
false
};
if is_double_click {
if let Some(handler) = &self.on_icon_double_click {
handler(&icon_id);
}
self.last_click_time = None;
self.last_click_id = None;
} else {
self.select_icon(&icon_id);
if let Some(handler) = &self.on_icon_click {
handler(&icon_id);
}
self.last_click_time = Some(now);
self.last_click_id = Some(icon_id);
}
ctx.request_redraw();
return EventResult::Handled;
} else {
self.clear_selection();
ctx.request_redraw();
}
}
MouseEventKind::Down if mouse.button == Some(MouseButton::Right) => {
if let Some(handler) = &self.on_right_click {
handler(mouse.position);
}
return EventResult::Handled;
}
_ => {}
}
}
EventResult::Ignored
}
fn bounds(&self) -> Rect {
self.base.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.base.bounds = bounds;
}
}