1use std::hash::Hasher;
2
3use djb_hash::{x33a_u32::X33aU32, HasherU32};
4use gtk::{gdk, glib, graphene, gsk, pango, prelude::*};
5
6const NOTIFICATION_ICON_SIZE: i32 = 48;
8
9const AVATAR_COLOR_LIST: [(&str, &str, &str); 14] = [
12 ("#cfe1f5", "#83b6ec", "#337fdc"), ("#caeaf2", "#7ad9f1", "#0f9ac8"), ("#cef8d8", "#8de6b1", "#29ae74"), ("#e6f9d7", "#b5e98a", "#6ab85b"), ("#f9f4e1", "#f8e359", "#d29d09"), ("#ffead1", "#ffcb62", "#d68400"), ("#ffe5c5", "#ffa95a", "#ed5b00"), ("#f8d2ce", "#f78773", "#e62d42"), ("#fac7de", "#e973ab", "#e33b6a"), ("#e7c2e8", "#cb78d4", "#9945b5"), ("#d5d2f5", "#9e91e8", "#7a59ca"), ("#f2eade", "#e3cf9c", "#b08952"), ("#e5d6ca", "#be916d", "#785336"), ("#d8d7d3", "#c0bfbc", "#6e6d71"), ];
27
28pub(crate) fn paintable_as_notification_icon(
30 paintable: &gdk::Paintable,
31 scale_factor: i32,
32 renderer: &gsk::Renderer,
33) -> gdk::Texture {
34 let img_width = f64::from(paintable.intrinsic_width());
35 let img_height = f64::from(paintable.intrinsic_height());
36
37 let mut icon_size = f64::from(NOTIFICATION_ICON_SIZE * scale_factor);
38 let mut snap_width = img_width;
39 let mut snap_height = img_height;
40 let mut x_pos = 0.0;
41 let mut y_pos = 0.0;
42
43 if img_width > img_height {
44 if img_height > icon_size {
47 snap_height = icon_size;
48 snap_width = img_width * icon_size / img_height;
49 } else {
50 icon_size = img_height;
51 }
52
53 if snap_width > icon_size {
55 x_pos = ((snap_width - icon_size) / 2.0) as f32;
56 }
57 } else {
58 if img_width > icon_size {
61 snap_width = icon_size;
62 snap_height = img_height * icon_size / img_width;
63 } else {
64 icon_size = img_width;
65 }
66
67 if snap_height > icon_size {
69 y_pos = ((snap_height - icon_size) / 2.0) as f32;
70 }
71 }
72
73 let icon_size = icon_size as f32;
74 let snapshot = gtk::Snapshot::new();
75
76 let bounds = gsk::RoundedRect::from_rect(
78 graphene::Rect::new(x_pos, y_pos, icon_size, icon_size),
79 icon_size / 2.0,
80 );
81 snapshot.push_rounded_clip(&bounds);
82
83 paintable.snapshot(&snapshot, snap_width, snap_height);
84
85 snapshot.pop();
86
87 let node = snapshot
89 .to_node()
90 .expect("snapshot should convert to a node successfully");
91 renderer.render_texture(node, None)
92}
93
94pub(crate) fn string_as_notification_icon(
98 string: &str,
99 scale_factor: i32,
100 layout: &pango::Layout,
101 renderer: &gsk::Renderer,
102) -> gdk::Texture {
103 let mut hasher = X33aU32::new();
105 hasher.write(string.as_bytes());
106 let color_nb = hasher.finish_u32() as usize % AVATAR_COLOR_LIST.len();
107 let colors = AVATAR_COLOR_LIST[color_nb];
108
109 let icon_size = (NOTIFICATION_ICON_SIZE * scale_factor) as f32;
110 let snapshot = gtk::Snapshot::new();
111
112 let bounds = gsk::RoundedRect::from_rect(
114 graphene::Rect::new(0.0, 0.0, icon_size, icon_size),
115 icon_size / 2.0,
116 );
117 snapshot.push_rounded_clip(&bounds);
118
119 snapshot.append_linear_gradient(
121 &graphene::Rect::new(0.0, 0.0, icon_size, icon_size),
122 &graphene::Point::new(0.0, 0.0),
123 &graphene::Point::new(0.0, icon_size),
124 &[
125 gsk::ColorStop::new(
126 0.0,
127 gdk::RGBA::parse(colors.1).expect("hex color should parse successfully"),
128 ),
129 gsk::ColorStop::new(
130 1.0,
131 gdk::RGBA::parse(colors.2).expect("hex color should parse successfully"),
132 ),
133 ],
134 );
135
136 snapshot.pop();
137
138 let normalized = glib::normalize(string, glib::NormalizeMode::DefaultCompose);
141 let first_initial = normalized
142 .chars()
143 .next()
144 .and_then(|c| c.to_uppercase().next());
145 let last_initial = normalized
146 .rfind(' ')
147 .and_then(|idx| (idx != normalized.len()).then(|| &normalized[idx + 1..]))
148 .and_then(|s| s.chars().next())
149 .and_then(|c| c.to_uppercase().next());
150 let initials = first_initial
151 .into_iter()
152 .chain(last_initial)
153 .collect::<String>();
154 layout.set_text(&initials);
155
156 if let Some(mut font_description) = layout
158 .font_description()
159 .or_else(|| layout.context().font_description())
160 {
161 font_description.set_weight(pango::Weight::Bold);
162 font_description.set_size(18 * scale_factor * pango::SCALE);
163 layout.set_font_description(Some(&font_description));
164 }
165
166 layout.set_width(icon_size as i32 * pango::SCALE);
168 layout.set_alignment(pango::Alignment::Center);
169
170 let (_, lay_height) = layout.pixel_size();
172 let lay_baseline = layout.baseline() / pango::SCALE;
173 let lay_padding = lay_height - lay_baseline;
176 let pos_y = (icon_size - lay_height as f32 - lay_padding as f32) / 2.0;
177 snapshot.translate(&graphene::Point::new(0.0, pos_y));
178
179 snapshot.append_layout(
180 layout,
181 &gdk::RGBA::parse(colors.0).expect("hex color should parse successfully"),
182 );
183
184 let node = snapshot
186 .to_node()
187 .expect("snapshot should convert to a node successfully");
188 renderer.render_texture(node, None)
189}