| 1 | //! Power control buttons widget |
| 2 | //! |
| 3 | //! Shutdown, reboot, and suspend buttons with hover effects. |
| 4 | |
| 5 | use crate::icons; |
| 6 | use crate::render::rounded_rectangle; |
| 7 | use anyhow::Result; |
| 8 | use cairo::Context; |
| 9 | |
| 10 | /// Power button action types |
| 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 12 | pub enum PowerAction { |
| 13 | Shutdown, |
| 14 | Reboot, |
| 15 | Suspend, |
| 16 | } |
| 17 | |
| 18 | /// Individual power button |
| 19 | struct PowerButton { |
| 20 | action: PowerAction, |
| 21 | x: f64, |
| 22 | y: f64, |
| 23 | size: f64, |
| 24 | hovered: bool, |
| 25 | } |
| 26 | |
| 27 | impl PowerButton { |
| 28 | fn new(action: PowerAction, x: f64, y: f64, size: f64) -> Self { |
| 29 | Self { |
| 30 | action, |
| 31 | x, |
| 32 | y, |
| 33 | size, |
| 34 | hovered: false, |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | fn contains(&self, px: f64, py: f64) -> bool { |
| 39 | px >= self.x && px <= self.x + self.size && py >= self.y && py <= self.y + self.size |
| 40 | } |
| 41 | |
| 42 | fn render(&self, ctx: &Context) -> Result<()> { |
| 43 | let icon_size = self.size * 0.5; |
| 44 | let icon_x = self.x + (self.size - icon_size) / 2.0; |
| 45 | let icon_y = self.y + (self.size - icon_size) / 2.0; |
| 46 | |
| 47 | // Background with hover effect |
| 48 | if self.hovered { |
| 49 | // Glow effect on hover |
| 50 | ctx.set_source_rgba(1.0, 1.0, 1.0, 0.15); |
| 51 | rounded_rectangle(ctx, self.x, self.y, self.size, self.size, 12.0); |
| 52 | ctx.fill()?; |
| 53 | |
| 54 | // Brighter icon on hover |
| 55 | let (r, g, b) = self.hover_color(); |
| 56 | ctx.set_source_rgb(r, g, b); |
| 57 | } else { |
| 58 | // Normal semi-transparent background |
| 59 | ctx.set_source_rgba(1.0, 1.0, 1.0, 0.05); |
| 60 | rounded_rectangle(ctx, self.x, self.y, self.size, self.size, 12.0); |
| 61 | ctx.fill()?; |
| 62 | |
| 63 | // Normal icon color |
| 64 | ctx.set_source_rgba(0.8, 0.8, 0.8, 0.9); |
| 65 | } |
| 66 | |
| 67 | // Draw the icon |
| 68 | match self.action { |
| 69 | PowerAction::Shutdown => icons::draw_power(ctx, icon_x, icon_y, icon_size), |
| 70 | PowerAction::Reboot => icons::draw_reboot(ctx, icon_x, icon_y, icon_size), |
| 71 | PowerAction::Suspend => icons::draw_suspend(ctx, icon_x, icon_y, icon_size), |
| 72 | } |
| 73 | |
| 74 | Ok(()) |
| 75 | } |
| 76 | |
| 77 | /// Get the hover highlight color for each action |
| 78 | fn hover_color(&self) -> (f64, f64, f64) { |
| 79 | match self.action { |
| 80 | PowerAction::Shutdown => (1.0, 0.4, 0.4), // Red tint |
| 81 | PowerAction::Reboot => (0.4, 0.8, 1.0), // Blue tint |
| 82 | PowerAction::Suspend => (0.9, 0.8, 0.4), // Yellow/gold tint |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | /// Power buttons panel |
| 88 | pub struct PowerButtons { |
| 89 | buttons: Vec<PowerButton>, |
| 90 | } |
| 91 | |
| 92 | impl PowerButtons { |
| 93 | /// Create power buttons positioned in bottom-right corner of given area |
| 94 | /// For multi-monitor: pass the primary monitor's x, y, width, height |
| 95 | pub fn new(area_x: f64, area_y: f64, area_width: f64, area_height: f64) -> Self { |
| 96 | let button_size = 48.0; |
| 97 | let spacing = 12.0; |
| 98 | let margin = 24.0; |
| 99 | |
| 100 | // Position in bottom-right corner of the area |
| 101 | let start_x = area_x + area_width - margin - (button_size * 3.0 + spacing * 2.0); |
| 102 | let y = area_y + area_height - margin - button_size; |
| 103 | |
| 104 | let buttons = vec![ |
| 105 | PowerButton::new(PowerAction::Suspend, start_x, y, button_size), |
| 106 | PowerButton::new( |
| 107 | PowerAction::Reboot, |
| 108 | start_x + button_size + spacing, |
| 109 | y, |
| 110 | button_size, |
| 111 | ), |
| 112 | PowerButton::new( |
| 113 | PowerAction::Shutdown, |
| 114 | start_x + (button_size + spacing) * 2.0, |
| 115 | y, |
| 116 | button_size, |
| 117 | ), |
| 118 | ]; |
| 119 | |
| 120 | Self { buttons } |
| 121 | } |
| 122 | |
| 123 | /// Render all power buttons |
| 124 | pub fn render(&self, ctx: &Context) -> Result<()> { |
| 125 | for button in &self.buttons { |
| 126 | button.render(ctx)?; |
| 127 | } |
| 128 | Ok(()) |
| 129 | } |
| 130 | |
| 131 | /// Update hover state based on mouse position |
| 132 | /// Returns true if any hover state changed |
| 133 | pub fn update_hover(&mut self, mouse_x: f64, mouse_y: f64) -> bool { |
| 134 | let mut changed = false; |
| 135 | for button in &mut self.buttons { |
| 136 | let was_hovered = button.hovered; |
| 137 | button.hovered = button.contains(mouse_x, mouse_y); |
| 138 | if button.hovered != was_hovered { |
| 139 | changed = true; |
| 140 | } |
| 141 | } |
| 142 | changed |
| 143 | } |
| 144 | |
| 145 | /// Clear all hover states |
| 146 | pub fn clear_hover(&mut self) { |
| 147 | for button in &mut self.buttons { |
| 148 | button.hovered = false; |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | /// Check if a click hit any button and return the action |
| 153 | pub fn handle_click(&self, click_x: f64, click_y: f64) -> Option<PowerAction> { |
| 154 | for button in &self.buttons { |
| 155 | if button.contains(click_x, click_y) { |
| 156 | return Some(button.action); |
| 157 | } |
| 158 | } |
| 159 | None |
| 160 | } |
| 161 | |
| 162 | /// Get which button is currently hovered (for cursor changes) |
| 163 | pub fn hovered_action(&self) -> Option<PowerAction> { |
| 164 | self.buttons |
| 165 | .iter() |
| 166 | .find(|b| b.hovered) |
| 167 | .map(|b| b.action) |
| 168 | } |
| 169 | |
| 170 | /// Get the hovered button info for tooltip rendering (action, center_x, top_y) |
| 171 | pub fn hovered_tooltip_info(&self) -> Option<(PowerAction, f64, f64)> { |
| 172 | self.buttons.iter().find(|b| b.hovered).map(|b| { |
| 173 | let center_x = b.x + b.size / 2.0; |
| 174 | let top_y = b.y; |
| 175 | (b.action, center_x, top_y) |
| 176 | }) |
| 177 | } |
| 178 | } |
| 179 |