notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }