Skip to content

Commit

Permalink
feat: improve icon handling
Browse files Browse the repository at this point in the history
  • Loading branch information
amrbashir committed Mar 5, 2022
1 parent 371caaf commit a52b763
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 69 deletions.
5 changes: 5 additions & 0 deletions .changes/notificatiosn-icon.md
@@ -0,0 +1,5 @@
---
"win7-notifications": "minor"
---

**Breaking change** Notification icon must be "32bpp RGBA data" and width and height must be specified now, check `Notification::icon` for more info.
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -38,3 +38,4 @@ features = [

[dev-dependencies]
tao = "0.6.2"
image = "0.24"
Binary file removed examples/icon.ico
Binary file not shown.
Binary file added examples/icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 14 additions & 2 deletions examples/multiple.rs
@@ -1,11 +1,14 @@
use std::path::Path;

use tao::{
event::{Event, StartCause},
event_loop::EventLoop,
};
use win7_notifications::{Notification, Timeout};
fn main() {
let event_loop = EventLoop::new();
let icon = include_bytes!("icon.ico");
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png");
let (icon, w, h) = load_icon(Path::new(path));

event_loop.run(move |event, _, _| match event {
Event::NewEvents(e) if e == StartCause::Init => {
Expand All @@ -14,7 +17,7 @@ fn main() {
.appname("App name")
.summary("Critical Error")
.body(format!("Just kidding, this is just the notification example {}.", i).as_str())
.icon(icon.to_vec())
.icon(icon.clone(), w, h)
.timeout(Timeout::Default)
.show()
.unwrap();
Expand All @@ -23,3 +26,12 @@ fn main() {
_ => (),
});
}

fn load_icon(path: &Path) -> (Vec<u8>, u32, u32) {
let image = image::open(path)
.expect("Failed to open icon path")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
}
16 changes: 14 additions & 2 deletions examples/single.rs
@@ -1,23 +1,35 @@
use std::path::Path;

use tao::{
event::{Event, StartCause},
event_loop::EventLoop,
};
use win7_notifications::{Notification, Timeout};
fn main() {
let event_loop = EventLoop::new();
let icon = include_bytes!("icon.ico");
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png");
let (icon, w, h) = load_icon(Path::new(path));

event_loop.run(move |event, _, _| match event {
Event::NewEvents(e) if e == StartCause::Init => {
Notification::new()
.appname("App name")
.summary("Critical Error")
.body("Just kidding, this is just the notification example.")
.icon(icon.to_vec())
.icon(icon.clone(), w, h)
.timeout(Timeout::Default)
.show()
.unwrap();
}
_ => (),
});
}

fn load_icon(path: &Path) -> (Vec<u8>, u32, u32) {
let image = image::open(path)
.expect("Failed to open icon path")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
}
2 changes: 1 addition & 1 deletion src/lib.rs
Expand Up @@ -8,7 +8,7 @@
//!
//! This crate requires a win32 event loop to be running on the thread, otherwise the notification will close immediately,
//! it is recommended to use it with other win32 event loop crates like [tao](https://docs.rs/tao) or
//! [winit](https://docs.rs/winit) or just roll your own win32 event loop.
//! [winit](https://docs.rs/winit) or just use your own win32 event loop.
//!
//! # Examples
//!
Expand Down
80 changes: 49 additions & 31 deletions src/notification.rs
Expand Up @@ -34,10 +34,7 @@ use windows::{
},
};

use crate::{
timeout::Timeout,
util::{self, Color},
};
use crate::{timeout::Timeout, util};

/// notification width
const NW: i32 = 360;
Expand All @@ -48,11 +45,11 @@ const NM: i32 = 16;
/// notification icon size (width/height)
const NIS: i32 = 16;
/// notification window bg color
const WC: Color = Color(50, 57, 69);
const WC: u32 = util::RGB(50, 57, 69);
/// used for notification summary (title)
const TC: Color = Color(255, 255, 255);
const TC: u32 = util::RGB(255, 255, 255);
/// used for notification body
const SC: Color = Color(200, 200, 200);
const SC: u32 = util::RGB(200, 200, 200);

static ACTIVE_NOTIFICATIONS: Lazy<Mutex<Vec<HWND>>> = Lazy::new(|| Mutex::new(Vec::new()));
static PRIMARY_MONITOR: Lazy<Mutex<MONITORINFOEXW>> =
Expand All @@ -63,6 +60,8 @@ static PRIMARY_MONITOR: Lazy<Mutex<MONITORINFOEXW>> =
#[derive(Debug, Clone)]
pub struct Notification {
pub icon: Vec<u8>,
pub icon_width: u32,
pub icon_height: u32,
pub appname: String,
pub summary: String,
pub body: String,
Expand All @@ -76,6 +75,8 @@ impl Default for Notification {
summary: String::new(),
body: String::new(),
icon: Vec::new(),
icon_height: 32,
icon_width: 32,
timeout: Timeout::Default,
}
}
Expand Down Expand Up @@ -114,9 +115,22 @@ impl Notification {
self
}

/// Set the `icon` field must be `.ico` byte array.
pub fn icon(&mut self, icon: Vec<u8>) -> &mut Notification {
self.icon = icon;
/// Set the `icon` field from 32bpp RGBA data.
///
/// The length of `rgba` must be divisible by 4, and `width * height` must equal
/// `rgba.len() / 4`. Otherwise, this will panic.
pub fn icon(&mut self, rgba: Vec<u8>, width: u32, height: u32) -> &mut Notification {
if rgba.len() % util::PIXEL_SIZE != 0 {
panic!();
}
let pixel_count = rgba.len() / util::PIXEL_SIZE;
if pixel_count != (width * height) as usize {
panic!()
} else {
self.icon = rgba;
self.icon_width = width;
self.icon_height = height;
}
self
}

Expand All @@ -138,7 +152,7 @@ impl Notification {
lpfnWndProc: Some(window_proc),
lpszClassName: PWSTR(class_name.as_mut_ptr() as _),
hInstance: hinstance,
hbrBackground: CreateSolidBrush(WC.to_int()),
hbrBackground: CreateSolidBrush(WC),
..Default::default()
};
RegisterClassW(&wnd_class);
Expand Down Expand Up @@ -200,7 +214,8 @@ impl Notification {

util::skip_taskbar(hwnd)?;
ShowWindow(hwnd, SW_SHOW);
// note(amrbashir): Passing an invalid path to `PlaySoundW` will make windows play default sound.
// Passing an invalid path to `PlaySoundW` will make windows play default sound.
// https://docs.microsoft.com/en-us/previous-versions/dd743680(v=vs.85)#remarks
PlaySoundW("NULL", HINSTANCE::default(), SND_ASYNC);

let timeout = self.timeout;
Expand Down Expand Up @@ -282,37 +297,40 @@ pub unsafe extern "system" fn window_proc(
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(hwnd, &mut ps);

SetBkColor(hdc, WC.to_int());
SetTextColor(hdc, TC.to_int());
SetBkColor(hdc, WC);
SetTextColor(hdc, TC);

// draw notification icon
if let Some(hicon) = util::get_hicon_from_buffer(&(*userdata).notification.icon, 16, 16) {
DrawIconEx(
hdc,
NM,
NM,
hicon,
NIS,
NIS,
0,
HBRUSH::default(),
DI_NORMAL,
);
}
let hicon = util::get_hicon_from_buffer(
(*userdata).notification.icon.clone(),
(*userdata).notification.icon_width,
(*userdata).notification.icon_height,
);
DrawIconEx(
hdc,
NM,
NM,
hicon,
NIS,
NIS,
0,
HBRUSH::default(),
DI_NORMAL,
);

// draw notification close button
SetTextColor(
hdc,
if (*userdata).mouse_hovering_close_btn {
TC.to_int()
TC
} else {
SC.to_int()
SC
},
);
TextOutW(hdc, NW - NM - NM / 2, NM, "X", 1);

// draw notification app name
SetTextColor(hdc, TC.to_int());
SetTextColor(hdc, TC);
util::set_font(hdc, "Segeo UI", 15, 400);
TextOutW(
hdc,
Expand All @@ -333,7 +351,7 @@ pub unsafe extern "system" fn window_proc(
);

// draw notification body
SetTextColor(hdc, SC.to_int());
SetTextColor(hdc, SC);
util::set_font(hdc, "Segeo UI", 17, 400);
let mut rc = RECT {
left: NM,
Expand Down
2 changes: 1 addition & 1 deletion src/timeout.rs
Expand Up @@ -7,7 +7,7 @@
pub enum Timeout {
/// Expires according to server default.
///
/// Whatever that might be...
/// 5000ms
Default,

/// Do not expire, user will have to close this manually.
Expand Down
72 changes: 40 additions & 32 deletions src/util.rs
Expand Up @@ -21,13 +21,11 @@ pub fn current_exe_name() -> String {
.to_owned()
}

pub struct Color(pub u32, pub u32, pub u32);

impl Color {
/// conver Color to a integer based color
pub fn to_int(&self) -> u32 {
self.0 | (self.1 << 8) | (self.2 << 16)
}
/// Implementation of the `RGB` macro.
#[allow(non_snake_case)]
#[inline]
pub const fn RGB(r: u32, g: u32, b: u32) -> u32 {
r | g << 8 | b << 16
}

#[cfg(target_pointer_width = "32")]
Expand Down Expand Up @@ -119,31 +117,41 @@ pub unsafe fn set_font(hdc: Gdi::HDC, name: &str, size: i32, weight: i32) {
Gdi::SelectObject(hdc, hfont);
}

pub fn get_hicon_from_buffer(buffer: &[u8], width: i32, height: i32) -> Option<w32wm::HICON> {
pub(crate) struct Pixel {
pub(crate) r: u8,
pub(crate) g: u8,
pub(crate) b: u8,
pub(crate) a: u8,
}

impl Pixel {
fn to_bgra(&mut self) {
std::mem::swap(&mut self.r, &mut self.b);
}
}

pub(crate) const PIXEL_SIZE: usize = std::mem::size_of::<Pixel>();

pub fn get_hicon_from_buffer(rgba: Vec<u8>, width: u32, height: u32) -> w32wm::HICON {
let mut rgba = rgba;
let pixel_count = rgba.len() / PIXEL_SIZE;
let mut and_mask = Vec::with_capacity(pixel_count);
let pixels =
unsafe { std::slice::from_raw_parts_mut(rgba.as_mut_ptr() as *mut Pixel, pixel_count) };
for pixel in pixels {
and_mask.push(pixel.a.wrapping_sub(std::u8::MAX)); // invert alpha channel
pixel.to_bgra();
}
assert_eq!(and_mask.len(), pixel_count);
unsafe {
match w32wm::LookupIconIdFromDirectoryEx(
buffer.as_ptr() as _,
true,
width,
height,
w32wm::LR_DEFAULTCOLOR,
) as isize
{
0 => None,
offset => {
match w32wm::CreateIconFromResourceEx(
buffer.as_ptr().offset(offset) as _,
buffer.len() as _,
true,
0x00030000,
0,
0,
w32wm::LR_DEFAULTCOLOR,
) {
hicon if hicon.is_invalid() => None,
hicon => Some(hicon),
}
}
}
w32wm::CreateIcon(
w32f::HINSTANCE::default(),
width as i32,
height as i32,
1,
(4 * 8) as u8,
and_mask.as_ptr() as *const u8,
rgba.as_ptr() as *const u8,
) as w32wm::HICON
}
}

0 comments on commit a52b763

Please sign in to comment.