icons.rs (12245B)
1 use egui::{pos2, vec2, Color32, CursorIcon, Pos2, Stroke, Widget}; 2 3 use crate::AnimationHelper; 4 5 pub static ICON_WIDTH: f32 = 40.0; 6 pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; 7 8 /// Creates a magnifying glass icon widget 9 pub fn search_icon(size: f32, height: f32) -> impl egui::Widget { 10 move |ui: &mut egui::Ui| { 11 // Use the provided height parameter 12 let desired_size = vec2(size, height); 13 let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::hover()); 14 15 // Calculate center position - this ensures the icon is centered in its allocated space 16 let center_pos = rect.center(); 17 let stroke = Stroke::new(1.5, Color32::from_rgb(150, 150, 150)); 18 19 // Draw circle 20 let circle_radius = size * 0.35; 21 ui.painter() 22 .circle(center_pos, circle_radius, Color32::TRANSPARENT, stroke); 23 24 // Draw handle 25 let handle_start = center_pos + vec2(circle_radius * 0.7, circle_radius * 0.7); 26 let handle_end = handle_start + vec2(size * 0.25, size * 0.25); 27 ui.painter() 28 .line_segment([handle_start, handle_end], stroke); 29 30 response 31 } 32 } 33 34 fn toolbar_icon_color(ui: &egui::Ui, is_active: bool) -> Color32 { 35 if is_active { 36 ui.visuals().strong_text_color() 37 } else { 38 ui.visuals().text_color() 39 } 40 } 41 42 /// Painter-drawn bell icon for notifications (filled when active) 43 pub fn notifications_button( 44 ui: &mut egui::Ui, 45 size: f32, 46 is_active: bool, 47 unseen_indicator: bool, 48 ) -> egui::Response { 49 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; 50 let helper = AnimationHelper::new(ui, "notifications-button", vec2(max_size, max_size)); 51 let rect = helper.get_animation_rect(); 52 let painter = ui.painter_at(rect); 53 let center = rect.center(); 54 let s = helper.scale_1d_pos(size); 55 let color = toolbar_icon_color(ui, is_active); 56 let stroke_width = helper.scale_1d_pos(1.5); 57 58 draw_bell(&painter, center, s, color, stroke_width, is_active); 59 60 if unseen_indicator { 61 let indicator_rect = rect.shrink((max_size - s) / 2.0); 62 paint_unseen_indicator(ui, indicator_rect, helper.scale_1d_pos(3.0)); 63 } 64 65 helper.take_animation_response() 66 } 67 68 fn draw_bell( 69 painter: &egui::Painter, 70 center: Pos2, 71 s: f32, 72 color: Color32, 73 stroke_width: f32, 74 filled: bool, 75 ) { 76 let bell_top = center.y - s * 0.4; 77 let bell_bottom = center.y + s * 0.25; 78 let dome_center = pos2(center.x, center.y - s * 0.1); 79 let dome_radius = s * 0.3; 80 let flare_half_w = s * 0.42; 81 82 let n_arc = 12; 83 let mut pts: Vec<Pos2> = Vec::with_capacity(n_arc + 4); 84 85 for i in 0..=n_arc { 86 let t = std::f32::consts::PI + (std::f32::consts::PI * i as f32 / n_arc as f32); 87 pts.push(pos2( 88 dome_center.x + dome_radius * t.cos(), 89 dome_center.y + dome_radius * t.sin(), 90 )); 91 } 92 pts.push(pos2(center.x + flare_half_w, bell_bottom)); 93 pts.push(pos2(center.x - flare_half_w, bell_bottom)); 94 95 if filled { 96 painter.add(egui::Shape::convex_polygon(pts, color, Stroke::NONE)); 97 } else { 98 let stroke = Stroke::new(stroke_width, color); 99 let n = pts.len(); 100 for i in 0..n { 101 painter.line_segment([pts[i], pts[(i + 1) % n]], stroke); 102 } 103 } 104 105 // Clapper 106 let clapper_center = pos2(center.x, bell_bottom + s * 0.12); 107 let clapper_radius = s * 0.08; 108 if filled { 109 painter.circle_filled(clapper_center, clapper_radius, color); 110 } else { 111 painter.circle_stroke( 112 clapper_center, 113 clapper_radius, 114 Stroke::new(stroke_width, color), 115 ); 116 } 117 118 // Nub on top 119 painter.circle_filled(pos2(center.x, bell_top), s * 0.05, color); 120 } 121 122 /// Painter-drawn envelope icon for chat/messages (filled when active) 123 pub fn chat_button(ui: &mut egui::Ui, size: f32, is_active: bool) -> egui::Response { 124 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; 125 let helper = AnimationHelper::new(ui, "chat-button", vec2(max_size, max_size)); 126 let rect = helper.get_animation_rect(); 127 let painter = ui.painter_at(rect); 128 let center = rect.center(); 129 let s = helper.scale_1d_pos(size); 130 let color = toolbar_icon_color(ui, is_active); 131 let stroke_width = helper.scale_1d_pos(1.5); 132 133 draw_envelope(&painter, ui, center, s, color, stroke_width, is_active); 134 135 helper.take_animation_response() 136 } 137 138 fn draw_envelope( 139 painter: &egui::Painter, 140 ui: &egui::Ui, 141 center: Pos2, 142 s: f32, 143 color: Color32, 144 stroke_width: f32, 145 filled: bool, 146 ) { 147 let half_w = s * 0.5; 148 let half_h = s * 0.35; 149 let env_rect = egui::Rect::from_center_size(center, vec2(half_w * 2.0, half_h * 2.0)); 150 let rounding = s * 0.08; 151 let flap_tip = pos2(center.x, center.y + s * 0.05); 152 153 if filled { 154 painter.rect_filled(env_rect, rounding, color); 155 let bg = if ui.visuals().dark_mode { 156 ui.visuals().window_fill 157 } else { 158 Color32::WHITE 159 }; 160 let flap = vec![ 161 pos2(env_rect.left(), env_rect.top()), 162 flap_tip, 163 pos2(env_rect.right(), env_rect.top()), 164 ]; 165 painter.add(egui::Shape::convex_polygon(flap, bg, Stroke::NONE)); 166 } else { 167 let stroke = Stroke::new(stroke_width, color); 168 painter.rect_stroke(env_rect, rounding, stroke, egui::StrokeKind::Inside); 169 painter.line_segment([pos2(env_rect.left(), env_rect.top()), flap_tip], stroke); 170 painter.line_segment([pos2(env_rect.right(), env_rect.top()), flap_tip], stroke); 171 } 172 } 173 174 /// Painter-drawn home icon (house outline, filled when active) 175 pub fn home_button(ui: &mut egui::Ui, size: f32, is_active: bool) -> egui::Response { 176 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; 177 let helper = AnimationHelper::new(ui, "home-button", vec2(max_size, max_size)); 178 let rect = helper.get_animation_rect(); 179 let painter = ui.painter_at(rect); 180 let center = rect.center(); 181 let s = helper.scale_1d_pos(size); 182 let color = toolbar_icon_color(ui, is_active); 183 let stroke_width = helper.scale_1d_pos(1.5); 184 185 draw_house(&painter, ui, center, s, color, stroke_width, is_active); 186 187 helper.take_animation_response() 188 } 189 190 fn draw_house( 191 painter: &egui::Painter, 192 ui: &egui::Ui, 193 center: Pos2, 194 s: f32, 195 color: Color32, 196 stroke_width: f32, 197 filled: bool, 198 ) { 199 let roof_top = pos2(center.x, center.y - s * 0.45); 200 let roof_left = pos2(center.x - s * 0.5, center.y - s * 0.02); 201 let roof_right = pos2(center.x + s * 0.5, center.y - s * 0.02); 202 203 let body_top = center.y - s * 0.02; 204 let body_bottom = center.y + s * 0.4; 205 let body_left = center.x - s * 0.38; 206 let body_right = center.x + s * 0.38; 207 208 let door_w = if filled { s * 0.2 } else { s * 0.15 }; 209 let door_h = if filled { s * 0.28 } else { s * 0.25 }; 210 211 if filled { 212 let roof = vec![roof_top, roof_left, roof_right]; 213 painter.add(egui::Shape::convex_polygon(roof, color, Stroke::NONE)); 214 let body = vec![ 215 pos2(body_left, body_top), 216 pos2(body_left, body_bottom), 217 pos2(body_right, body_bottom), 218 pos2(body_right, body_top), 219 ]; 220 painter.add(egui::Shape::convex_polygon(body, color, Stroke::NONE)); 221 // Door cutout 222 let bg = if ui.visuals().dark_mode { 223 ui.visuals().window_fill 224 } else { 225 Color32::WHITE 226 }; 227 let door = vec![ 228 pos2(center.x - door_w, body_bottom), 229 pos2(center.x - door_w, body_bottom - door_h), 230 pos2(center.x + door_w, body_bottom - door_h), 231 pos2(center.x + door_w, body_bottom), 232 ]; 233 painter.add(egui::Shape::convex_polygon(door, bg, Stroke::NONE)); 234 } else { 235 let stroke = Stroke::new(stroke_width, color); 236 // Roof 237 painter.line_segment([roof_top, roof_left], stroke); 238 painter.line_segment([roof_top, roof_right], stroke); 239 // Roof base connecting to walls 240 painter.line_segment([roof_left, pos2(body_left, body_top)], stroke); 241 painter.line_segment([roof_right, pos2(body_right, body_top)], stroke); 242 // Walls 243 painter.line_segment( 244 [pos2(body_left, body_top), pos2(body_left, body_bottom)], 245 stroke, 246 ); 247 painter.line_segment( 248 [pos2(body_left, body_bottom), pos2(body_right, body_bottom)], 249 stroke, 250 ); 251 painter.line_segment( 252 [pos2(body_right, body_bottom), pos2(body_right, body_top)], 253 stroke, 254 ); 255 // Door outline 256 painter.line_segment( 257 [ 258 pos2(center.x - door_w, body_bottom), 259 pos2(center.x - door_w, body_bottom - door_h), 260 ], 261 stroke, 262 ); 263 painter.line_segment( 264 [ 265 pos2(center.x - door_w, body_bottom - door_h), 266 pos2(center.x + door_w, body_bottom - door_h), 267 ], 268 stroke, 269 ); 270 painter.line_segment( 271 [ 272 pos2(center.x + door_w, body_bottom - door_h), 273 pos2(center.x + door_w, body_bottom), 274 ], 275 stroke, 276 ); 277 } 278 } 279 280 pub fn search_button(_color: Color32, line_width: f32, is_active: bool) -> impl Widget { 281 move |ui: &mut egui::Ui| -> egui::Response { 282 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; 283 let lw = if is_active { 284 line_width + 0.5 285 } else { 286 line_width 287 }; 288 let helper = AnimationHelper::new(ui, "search-button", vec2(max_size, max_size)); 289 let painter = ui.painter_at(helper.get_animation_rect()); 290 291 let cur_lw = helper.scale_1d_pos(lw); 292 let min_outer_circle_radius = helper.scale_radius(15.0); 293 let cur_outer_circle_radius = helper.scale_1d_pos(min_outer_circle_radius); 294 let cur_handle_length = helper.scale_1d_pos(7.0); 295 let circle_center = helper.scale_from_center(-2.0, -2.0); 296 297 let handle_vec = vec2( 298 std::f32::consts::FRAC_1_SQRT_2, 299 std::f32::consts::FRAC_1_SQRT_2, 300 ); 301 let handle_pos_1 = circle_center + (handle_vec * (cur_outer_circle_radius - 3.0)); 302 let handle_pos_2 = 303 circle_center + (handle_vec * (cur_outer_circle_radius + cur_handle_length)); 304 305 let icon_color = toolbar_icon_color(ui, is_active); 306 let stroke = Stroke::new(cur_lw, icon_color); 307 let fill = if is_active { 308 icon_color 309 } else { 310 Color32::TRANSPARENT 311 }; 312 313 painter.line_segment([handle_pos_1, handle_pos_2], stroke); 314 painter.circle(circle_center, min_outer_circle_radius, fill, stroke); 315 316 helper 317 .take_animation_response() 318 .on_hover_cursor(CursorIcon::PointingHand) 319 } 320 } 321 322 fn paint_unseen_indicator(ui: &mut egui::Ui, rect: egui::Rect, radius: f32) { 323 let center = rect.center(); 324 let top_right = rect.right_top(); 325 let distance = center.distance(top_right); 326 let midpoint = { 327 let mut cur = center; 328 cur.x += distance / 2.0; 329 cur.y -= distance / 2.0; 330 cur 331 }; 332 333 let painter = ui.painter_at(rect); 334 painter.circle_filled(midpoint, radius, crate::colors::PINK); 335 } 336 337 /// Image-based expanding button used for side panel icons. 338 pub fn expanding_button( 339 name: &'static str, 340 img_size: f32, 341 light_img: egui::Image, 342 dark_img: egui::Image, 343 ui: &mut egui::Ui, 344 unseen_indicator: bool, 345 ) -> egui::Response { 346 let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; 347 let img = if ui.visuals().dark_mode { 348 dark_img 349 } else { 350 light_img 351 }; 352 353 let helper = AnimationHelper::new(ui, name, egui::vec2(max_size, max_size)); 354 let cur_img_size = helper.scale_1d_pos(img_size); 355 let paint_rect = helper 356 .get_animation_rect() 357 .shrink((max_size - cur_img_size) / 2.0); 358 img.paint_at(ui, paint_rect); 359 360 if unseen_indicator { 361 paint_unseen_indicator(ui, paint_rect, helper.scale_1d_pos(3.0)); 362 } 363 364 helper.take_animation_response() 365 }