fractal/utils/
notifications.rs

1use std::hash::Hasher;
2
3use djb_hash::{x33a_u32::X33aU32, HasherU32};
4use gtk::{gdk, graphene, gsk, pango, prelude::*};
5
6/// The notification icon size, according to GNOME Shell's code.
7const NOTIFICATION_ICON_SIZE: i32 = 48;
8
9/// The colors for avatars, according to libadwaita.
10const AVATAR_COLOR_LIST: [(&str, &str, &str); 14] = [
11    ("#cfe1f5", "#83b6ec", "#337fdc"), // blue
12    ("#caeaf2", "#7ad9f1", "#0f9ac8"), // cyan
13    ("#cef8d8", "#8de6b1", "#29ae74"), // green
14    ("#e6f9d7", "#b5e98a", "#6ab85b"), // lime
15    ("#f9f4e1", "#f8e359", "#d29d09"), // yellow
16    ("#ffead1", "#ffcb62", "#d68400"), // gold
17    ("#ffe5c5", "#ffa95a", "#ed5b00"), // orange
18    ("#f8d2ce", "#f78773", "#e62d42"), // raspberry
19    ("#fac7de", "#e973ab", "#e33b6a"), // magenta
20    ("#e7c2e8", "#cb78d4", "#9945b5"), // purple
21    ("#d5d2f5", "#9e91e8", "#7a59ca"), // violet
22    ("#f2eade", "#e3cf9c", "#b08952"), // beige
23    ("#e5d6ca", "#be916d", "#785336"), // brown
24    ("#d8d7d3", "#c0bfbc", "#6e6d71"), // gray
25];
26
27/// Generate a notification icon from the given paintable.
28pub(crate) fn paintable_as_notification_icon(
29    paintable: &gdk::Paintable,
30    scale_factor: i32,
31    renderer: &gsk::Renderer,
32) -> gdk::Texture {
33    let img_width = f64::from(paintable.intrinsic_width());
34    let img_height = f64::from(paintable.intrinsic_height());
35
36    let mut icon_size = f64::from(NOTIFICATION_ICON_SIZE * scale_factor);
37    let mut snap_width = img_width;
38    let mut snap_height = img_height;
39    let mut x_pos = 0.0;
40    let mut y_pos = 0.0;
41
42    if img_width > img_height {
43        // Make the height fit the icon size without distorting the image, but
44        // don't upscale it.
45        if img_height > icon_size {
46            snap_height = icon_size;
47            snap_width = img_width * icon_size / img_height;
48        } else {
49            icon_size = img_height;
50        }
51
52        // Center the clip horizontally.
53        if snap_width > icon_size {
54            x_pos = ((snap_width - icon_size) / 2.0) as f32;
55        }
56    } else {
57        // Make the width fit the icon size without distorting the image, but
58        // don't upscale it.
59        if img_width > icon_size {
60            snap_width = icon_size;
61            snap_height = img_height * icon_size / img_width;
62        } else {
63            icon_size = img_width;
64        }
65
66        // Center the clip vertically.
67        if snap_height > icon_size {
68            y_pos = ((snap_height - icon_size) / 2.0) as f32;
69        }
70    }
71
72    let icon_size = icon_size as f32;
73    let snapshot = gtk::Snapshot::new();
74
75    // Clip the avatar in a circle.
76    let bounds = gsk::RoundedRect::from_rect(
77        graphene::Rect::new(x_pos, y_pos, icon_size, icon_size),
78        icon_size / 2.0,
79    );
80    snapshot.push_rounded_clip(&bounds);
81
82    paintable.snapshot(&snapshot, snap_width, snap_height);
83
84    snapshot.pop();
85
86    // Render the avatar.
87    let node = snapshot.to_node().unwrap();
88    renderer.render_texture(node, None)
89}
90
91/// Generate a notification icon from a string.
92///
93/// This should match the behavior of `AdwAvatar`.
94pub(crate) fn string_as_notification_icon(
95    string: &str,
96    scale_factor: i32,
97    layout: &pango::Layout,
98    renderer: &gsk::Renderer,
99) -> gdk::Texture {
100    // Get the avatar colors from the string hash.
101    let mut hasher = X33aU32::new();
102    hasher.write(string.as_bytes());
103    let color_nb = hasher.finish_u32() as usize % AVATAR_COLOR_LIST.len();
104    let colors = AVATAR_COLOR_LIST[color_nb];
105
106    let icon_size = (NOTIFICATION_ICON_SIZE * scale_factor) as f32;
107    let snapshot = gtk::Snapshot::new();
108
109    // Clip the avatar in a circle.
110    let bounds = gsk::RoundedRect::from_rect(
111        graphene::Rect::new(0.0, 0.0, icon_size, icon_size),
112        icon_size / 2.0,
113    );
114    snapshot.push_rounded_clip(&bounds);
115
116    // Construct linear gradient background.
117    snapshot.append_linear_gradient(
118        &graphene::Rect::new(0.0, 0.0, icon_size, icon_size),
119        &graphene::Point::new(0.0, 0.0),
120        &graphene::Point::new(0.0, icon_size),
121        &[
122            gsk::ColorStop::new(0.0, gdk::RGBA::parse(colors.1).unwrap()),
123            gsk::ColorStop::new(1.0, gdk::RGBA::parse(colors.2).unwrap()),
124        ],
125    );
126
127    snapshot.pop();
128
129    // Add initials.
130    let initials = string
131        .split(char::is_whitespace)
132        .filter_map(|s| s.chars().next())
133        .collect::<String>();
134    layout.set_text(&initials);
135
136    // Set the proper weight and size.
137    if let Some(mut font_description) = layout
138        .font_description()
139        .or_else(|| layout.context().font_description())
140    {
141        font_description.set_weight(pango::Weight::Bold);
142        font_description.set_size(18 * scale_factor * pango::SCALE);
143        layout.set_font_description(Some(&font_description));
144    }
145
146    // Center the layout horizontally.
147    layout.set_width(icon_size as i32 * pango::SCALE);
148    layout.set_alignment(pango::Alignment::Center);
149
150    // Center the layout vertically.
151    let (_, lay_height) = layout.pixel_size();
152    let lay_baseline = layout.baseline() / pango::SCALE;
153    // This is not really a padding but the layout reports a bigger height than
154    // it seems to take and this seems like a good approximation.
155    let lay_padding = lay_height - lay_baseline;
156    let pos_y = (icon_size - lay_height as f32 - lay_padding as f32) / 2.0;
157    snapshot.translate(&graphene::Point::new(0.0, pos_y));
158
159    snapshot.append_layout(layout, &gdk::RGBA::parse(colors.0).unwrap());
160
161    // Render the avatar.
162    let node = snapshot.to_node().unwrap();
163    renderer.render_texture(node, None)
164}