diff --git a/Cargo.lock b/Cargo.lock index 4c63ef8b..451c3ed2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4006,6 +4006,7 @@ dependencies = [ "time-humanize", "ureq 3.0.11", "url", + "windows 0.58.0", "winres", ] @@ -6038,6 +6039,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.1" @@ -6070,17 +6081,30 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.0", + "windows-interface 0.59.1", "windows-link", "windows-result 0.3.2", - "windows-strings", + "windows-strings 0.4.0", ] [[package]] @@ -6093,6 +6117,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -6104,6 +6139,17 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "windows-interface" version = "0.59.1" @@ -6140,6 +6186,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.2" @@ -6149,6 +6204,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.4.0" diff --git a/psst-gui/Cargo.toml b/psst-gui/Cargo.toml index d8b79934..11857bfd 100644 --- a/psst-gui/Cargo.toml +++ b/psst-gui/Cargo.toml @@ -53,10 +53,20 @@ raw-window-handle = "0.5.2" # Must stay compatible with Druid souvlaki = { version = "0.8.2", default-features = false, features = ["use_zbus"] } sanitize_html = "0.9.0" rustfm-scrobble = "1.1.1" + [target.'cfg(windows)'.build-dependencies] winres = { version = "0.1.12" } image = { version = "0.25.6" } +[target.'cfg(windows)'.dependencies] +windows = { version = "0.58.0", features = [ + "Win32_UI_Shell", + "Win32_Foundation", + "Win32_System_Com", + "Win32_Graphics_Gdi", + "Win32_UI_WindowsAndMessaging", +] } + [package.metadata.bundle] name = "Psst" identifier = "com.jpochyla.psst" diff --git a/psst-gui/src/cmd.rs b/psst-gui/src/cmd.rs index 9e897e79..1b85810c 100644 --- a/psst-gui/src/cmd.rs +++ b/psst-gui/src/cmd.rs @@ -51,12 +51,14 @@ pub const PLAY_TRACKS: Selector = Selector::new("app.play-track pub const PLAY_PREVIOUS: Selector = Selector::new("app.play-previous"); pub const PLAY_PAUSE: Selector = Selector::new("app.play-pause"); pub const PLAY_RESUME: Selector = Selector::new("app.play-resume"); +pub const PLAY_PAUSE_OR_RESUME: Selector = Selector::new("app.play-pause-or-resume"); pub const PLAY_NEXT: Selector = Selector::new("app.play-next"); pub const PLAY_STOP: Selector = Selector::new("app.play-stop"); pub const ADD_TO_QUEUE: Selector<(QueueEntry, PlaybackItem)> = Selector::new("app.add-to-queue"); pub const PLAY_QUEUE_BEHAVIOR: Selector = Selector::new("app.play-queue-behavior"); pub const PLAY_SEEK: Selector = Selector::new("app.play-seek"); pub const SKIP_TO_POSITION: Selector = Selector::new("app.skip-to-position"); +pub const INITIALIZE_TASKBAR: Selector = Selector::new("app.initialize-taskbar"); // Sorting control pub const SORT_BY_DATE_ADDED: Selector = Selector::new("app.sort-by-date-added"); diff --git a/psst-gui/src/controller/mod.rs b/psst-gui/src/controller/mod.rs index ea3dc830..322ceebb 100644 --- a/psst-gui/src/controller/mod.rs +++ b/psst-gui/src/controller/mod.rs @@ -12,6 +12,7 @@ mod on_update; mod playback; mod session; mod sort; +mod taskbar; pub use after_delay::AfterDelay; pub use alert_cleanup::AlertCleanupController; diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index a8fb08e6..951c6ffb 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -7,7 +7,7 @@ use crossbeam_channel::Sender; use druid::{ im::Vector, widget::{prelude::*, Controller}, - Code, ExtEventSink, InternalLifeCycle, KbKey, WindowHandle, + Code, ExtEventSink, InternalLifeCycle, KbKey, Target, WindowHandle, }; use psst_core::{ audio::{normalize::NormalizationLevel, output::DefaultAudioOutput}, @@ -25,6 +25,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use crate::{ cmd, + controller::taskbar::TaskbarManager, data::Nav, data::{ AppState, Config, NowPlaying, Playable, Playback, PlaybackOrigin, PlaybackState, @@ -38,6 +39,9 @@ pub struct PlaybackController { thread: Option>, output: Option, media_controls: Option, + taskbar_manager: Option, + taskbar_buttons_initialized: bool, + last_taskbar_state: Option, has_scrobbled: bool, scrobbler: Option, startup: bool, @@ -75,6 +79,9 @@ impl PlaybackController { thread: None, output: None, media_controls: None, + taskbar_manager: None, + taskbar_buttons_initialized: false, + last_taskbar_state: None, has_scrobbled: false, scrobbler: None, startup: true, @@ -104,6 +111,9 @@ impl PlaybackController { .map_err(|err| log::error!("failed to connect to media control interface: {err:?}")) .ok(); + self.taskbar_manager = TaskbarManager::new(window, event_sink.clone(), widget_id) + .map_err(|err| log::error!("failed to initialize taskbar manager: {:?}", err)) + .ok(); self.sender = Some(player.sender()); self.thread = Some(thread::spawn(move || { Self::service_events(player, event_sink, widget_id); @@ -222,6 +232,33 @@ impl PlaybackController { }) .unwrap_or_default(); } + + self.update_taskbar_buttons(playback.state); + } + + fn setup_taskbar_buttons_on_first_play(&mut self, playback_state: PlaybackState) { + if !self.taskbar_buttons_initialized { + if let Some(taskbar_manager) = &self.taskbar_manager { + if let Err(e) = taskbar_manager.setup_buttons(playback_state) { + log::error!("Failed to setup taskbar buttons: {:?}", e); + } else { + self.taskbar_buttons_initialized = true; + } + } + } + } + + fn update_taskbar_buttons(&mut self, playback_state: PlaybackState) { + if Some(playback_state) != self.last_taskbar_state { + if self.taskbar_buttons_initialized { + if let Some(taskbar_manager) = &self.taskbar_manager { + if let Err(e) = taskbar_manager.update_all_buttons(playback_state) { + log::error!("Failed to update taskbar buttons: {:?}", e); + } + } + } + self.last_taskbar_state = Some(playback_state); + } } fn update_media_control_metadata(&mut self, playback: &Playback) { @@ -506,6 +543,10 @@ where self.resume(); ctx.set_handled(); } + Event::Command(cmd) if cmd.is(cmd::PLAY_PAUSE_OR_RESUME) => { + self.pause_or_resume(); + ctx.set_handled(); + } Event::Command(cmd) if cmd.is(cmd::PLAY_PREVIOUS) => { self.previous(); ctx.set_handled(); @@ -547,6 +588,10 @@ where self.seek(Duration::from_millis(*location)); ctx.set_handled(); } + Event::Command(cmd) if cmd.is(cmd::INITIALIZE_TASKBAR) => { + self.setup_taskbar_buttons_on_first_play(PlaybackState::Stopped); + ctx.set_handled(); + } // Keyboard shortcuts. Event::KeyDown(key) if key.code == Code::Space => { self.pause_or_resume(); @@ -602,6 +647,17 @@ where self.set_volume(data.playback.volume); self.set_queue_behavior(data.playback.queue_behavior); + let event_sink = ctx.get_external_handle(); + let widget_id = ctx.widget_id(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(100)); + let _ = event_sink.submit_command( + cmd::INITIALIZE_TASKBAR, + (), + Target::Widget(widget_id), + ); + }); + // Request focus so we can receive keyboard events. ctx.submit_command(cmd::SET_FOCUS.to(ctx.widget_id())); } diff --git a/psst-gui/src/controller/taskbar.rs b/psst-gui/src/controller/taskbar.rs new file mode 100644 index 00000000..ff7ae24e --- /dev/null +++ b/psst-gui/src/controller/taskbar.rs @@ -0,0 +1,516 @@ +#[cfg(windows)] +use std::{ + collections::HashMap, + sync::{Arc, Mutex, OnceLock}, +}; + +#[cfg(windows)] +use druid::{ExtEventSink, Target, WidgetId}; + +#[cfg(windows)] +use windows::{ + core::*, Win32::Foundation::*, Win32::System::Com::*, Win32::UI::Shell::*, + Win32::UI::WindowsAndMessaging::*, +}; + +#[cfg(windows)] +use raw_window_handle::{HasRawWindowHandle, RawWindowHandle, Win32WindowHandle}; + +#[cfg(windows)] +use crate::{cmd, data::PlaybackState}; + +#[cfg(windows)] +const WM_COMMAND_OFFSET: u32 = 0x8000; +#[cfg(windows)] +const CMD_PREVIOUS: u32 = WM_COMMAND_OFFSET + 1; +#[cfg(windows)] +const CMD_PLAY_PAUSE: u32 = WM_COMMAND_OFFSET + 2; +#[cfg(windows)] +const CMD_NEXT: u32 = WM_COMMAND_OFFSET + 3; + +#[cfg(windows)] +static TASKBAR_CALLBACKS: OnceLock>>> = + OnceLock::new(); + +#[cfg(windows)] +pub struct TaskbarManager { + hwnd: HWND, + taskbar_list: Option, + event_sink: ExtEventSink, + widget_id: WidgetId, + is_initialized: bool, +} + +#[cfg(not(windows))] +use druid::{ExtEventSink, WidgetId}; + +#[cfg(not(windows))] +use raw_window_handle::HasRawWindowHandle; + +#[cfg(not(windows))] +use crate::data::PlaybackState; + +#[cfg(not(windows))] +pub struct TaskbarManager; + +impl TaskbarManager { + #[cfg(windows)] + pub fn new( + window_handle: &dyn HasRawWindowHandle, + event_sink: ExtEventSink, + widget_id: WidgetId, + ) -> Result { + let hwnd = match window_handle.raw_window_handle() { + RawWindowHandle::Win32(Win32WindowHandle { hwnd, .. }) => HWND(hwnd), + _ => { + log::error!("Failed to extract Win32 window handle"); + return Err(windows::core::Error::from_win32()); + } + }; + + let mut manager = Self { + hwnd, + taskbar_list: None, + event_sink, + widget_id, + is_initialized: false, + }; + + match manager.initialize() { + Ok(_) => Ok(manager), + Err(e) => { + log::error!("Failed to initialize TaskbarManager: {:?}", e); + Err(e) + } + } + } + + #[cfg(not(windows))] + pub fn new( + _window_handle: &dyn HasRawWindowHandle, + _event_sink: ExtEventSink, + _widget_id: WidgetId, + ) -> std::result::Result { + Ok(Self) + } + + #[cfg(windows)] + fn initialize(&mut self) -> Result<()> { + unsafe { + let com_result = CoInitializeEx(None, COINIT_APARTMENTTHREADED); + if !com_result.is_ok() { + log::warn!( + "COM initialization failed or already initialized: {:?}", + com_result + ); + } + + let taskbar_list: ITaskbarList3 = + match CoCreateInstance(&TaskbarList, None, CLSCTX_INPROC_SERVER) { + Ok(tl) => tl, + Err(e) => { + log::error!("Failed to create ITaskbarList3: {:?}", e); + return Err(e.into()); + } + }; + + match taskbar_list.HrInit() { + Ok(_) => {} + Err(e) => { + log::error!("Failed to initialize ITaskbarList3: {:?}", e); + return Err(e.into()); + } + } + + self.taskbar_list = Some(taskbar_list); + + let callbacks = TASKBAR_CALLBACKS.get_or_init(|| Arc::new(Mutex::new(HashMap::new()))); + callbacks.lock().unwrap().insert( + self.hwnd.0 as isize, + (self.event_sink.clone(), self.widget_id), + ); + + self.subclass_window()?; + self.is_initialized = true; + } + + Ok(()) + } + + #[cfg(not(windows))] + #[allow(dead_code)] + fn initialize(&mut self) -> std::result::Result<(), ()> { + Ok(()) + } + + #[cfg(windows)] + fn subclass_window(&self) -> Result<()> { + unsafe { + let result = SetWindowSubclass(self.hwnd, Some(taskbar_subclass_proc), 1, 0); + + if result.as_bool() { + Ok(()) + } else { + log::error!("Failed to subclass window"); + Err(windows::core::Error::from_win32()) + } + } + } + + #[cfg(windows)] + pub fn setup_buttons(&self, playback_state: PlaybackState) -> Result<()> { + if !self.is_initialized { + log::warn!("Taskbar manager not initialized, skipping button setup"); + return Ok(()); + } + + let taskbar_list = match &self.taskbar_list { + Some(tl) => tl, + None => { + log::error!("ITaskbarList3 interface is None"); + return Err(windows::core::Error::from_win32()); + } + }; + + unsafe { + let play_icon = taskbar_icons::create_play_icon(); + let pause_icon = taskbar_icons::create_pause_icon(); + let prev_icon = taskbar_icons::create_previous_icon(); + let next_icon = taskbar_icons::create_next_icon(); + + let (play_pause_icon, play_pause_tooltip) = match playback_state { + PlaybackState::Playing => (pause_icon, "Pause"), + _ => (play_icon, "Play"), + }; + + let buttons_enabled = playback_state != PlaybackState::Stopped; + let button_flags = if buttons_enabled { + THBF_ENABLED + } else { + THBF_DISABLED + }; + + let mut buttons = [ + THUMBBUTTON { + iId: CMD_PREVIOUS, + hIcon: prev_icon, + szTip: [0; 260], + dwMask: THB_ICON | THB_TOOLTIP | THB_FLAGS, + dwFlags: button_flags, + ..Default::default() + }, + THUMBBUTTON { + iId: CMD_PLAY_PAUSE, + hIcon: play_pause_icon, + szTip: [0; 260], + dwMask: THB_ICON | THB_TOOLTIP | THB_FLAGS, + dwFlags: button_flags, + ..Default::default() + }, + THUMBBUTTON { + iId: CMD_NEXT, + hIcon: next_icon, + szTip: [0; 260], + dwMask: THB_ICON | THB_TOOLTIP | THB_FLAGS, + dwFlags: button_flags, + ..Default::default() + }, + ]; + + self.set_button_tooltip(&mut buttons[0], "Previous")?; + self.set_button_tooltip(&mut buttons[1], play_pause_tooltip)?; + self.set_button_tooltip(&mut buttons[2], "Next")?; + + let result = taskbar_list.ThumbBarAddButtons(self.hwnd, &buttons); + + match result { + Ok(_) => {} + Err(e) => { + log::error!( + "Failed to add taskbar buttons: HRESULT 0x{:08x}", + e.code().0 + ); + return Err(e.into()); + } + } + } + + Ok(()) + } + + #[cfg(not(windows))] + pub fn setup_buttons(&self, _playback_state: PlaybackState) -> std::result::Result<(), ()> { + Ok(()) + } + + #[cfg(windows)] + pub fn update_all_buttons(&self, playback_state: PlaybackState) -> Result<()> { + if !self.is_initialized { + log::warn!("Taskbar manager not initialized, skipping button update"); + return Ok(()); + } + + let taskbar_list = match &self.taskbar_list { + Some(tl) => tl, + None => { + log::error!("ITaskbarList3 interface is None during update"); + return Err(windows::core::Error::from_win32()); + } + }; + + unsafe { + let play_icon = taskbar_icons::create_play_icon(); + let pause_icon = taskbar_icons::create_pause_icon(); + let prev_icon = taskbar_icons::create_previous_icon(); + let next_icon = taskbar_icons::create_next_icon(); + + let (play_pause_icon, play_pause_tooltip) = match playback_state { + PlaybackState::Playing => (pause_icon, "Pause"), + _ => (play_icon, "Play"), + }; + + let buttons_enabled = playback_state != PlaybackState::Stopped; + let button_flags = if buttons_enabled { + THBF_ENABLED + } else { + THBF_DISABLED + }; + + let mut buttons = [ + THUMBBUTTON { + iId: CMD_PREVIOUS, + hIcon: prev_icon, + szTip: [0; 260], + dwMask: THB_ICON | THB_TOOLTIP | THB_FLAGS, + dwFlags: button_flags, + ..Default::default() + }, + THUMBBUTTON { + iId: CMD_PLAY_PAUSE, + hIcon: play_pause_icon, + szTip: [0; 260], + dwMask: THB_ICON | THB_TOOLTIP | THB_FLAGS, + dwFlags: button_flags, + ..Default::default() + }, + THUMBBUTTON { + iId: CMD_NEXT, + hIcon: next_icon, + szTip: [0; 260], + dwMask: THB_ICON | THB_TOOLTIP | THB_FLAGS, + dwFlags: button_flags, + ..Default::default() + }, + ]; + + self.set_button_tooltip(&mut buttons[0], "Previous")?; + self.set_button_tooltip(&mut buttons[1], play_pause_tooltip)?; + self.set_button_tooltip(&mut buttons[2], "Next")?; + + match taskbar_list.ThumbBarUpdateButtons(self.hwnd, &buttons) { + Ok(_) => {} + Err(e) => { + log::error!("Failed to update all taskbar buttons: {:?}", e); + return Err(e.into()); + } + } + } + + Ok(()) + } + + #[cfg(not(windows))] + pub fn update_all_buttons(&self, _playback_state: PlaybackState) -> std::result::Result<(), ()> { + Ok(()) + } + + #[cfg(windows)] + fn set_button_tooltip(&self, button: &mut THUMBBUTTON, tooltip: &str) -> Result<()> { + let tooltip_bytes = tooltip.as_bytes(); + let len = tooltip_bytes.len().min(259); + + button.szTip.fill(0); + + for (i, &byte) in tooltip_bytes.iter().take(len).enumerate() { + button.szTip[i] = byte as u16; + } + + Ok(()) + } +} + +impl Drop for TaskbarManager { + #[cfg(windows)] + fn drop(&mut self) { + if self.is_initialized { + unsafe { + let _ = RemoveWindowSubclass(self.hwnd, Some(taskbar_subclass_proc), 1); + + if let Some(callbacks) = TASKBAR_CALLBACKS.get() { + callbacks.lock().unwrap().remove(&(self.hwnd.0 as isize)); + } + + CoUninitialize(); + } + } + } + + #[cfg(not(windows))] + fn drop(&mut self) {} +} + +#[cfg(windows)] +unsafe extern "system" fn taskbar_subclass_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _uid_subclass: usize, + _dw_ref_data: usize, +) -> LRESULT { + if msg == WM_COMMAND { + let command_id = (wparam.0 & 0xFFFF) as u32; + + if let Some(callbacks) = TASKBAR_CALLBACKS.get() { + if let Some((event_sink, widget_id)) = + callbacks.lock().unwrap().get(&(hwnd.0 as isize)).cloned() + { + match command_id { + CMD_PLAY_PAUSE => { + if let Err(e) = event_sink.submit_command( + cmd::PLAY_PAUSE_OR_RESUME, + (), + Target::Widget(widget_id), + ) { + log::error!("Failed to submit play/pause command: {:?}", e); + } + } + CMD_PREVIOUS => { + if let Err(e) = event_sink.submit_command( + cmd::PLAY_PREVIOUS, + (), + Target::Widget(widget_id), + ) { + log::error!("Failed to submit previous command: {:?}", e); + } + } + CMD_NEXT => { + if let Err(e) = + event_sink.submit_command(cmd::PLAY_NEXT, (), Target::Widget(widget_id)) + { + log::error!("Failed to submit next command: {:?}", e); + } + } + _ => {} + } + return LRESULT(0); + } + } + } + + DefSubclassProc(hwnd, msg, wparam, lparam) +} + +#[cfg(windows)] +mod taskbar_icons { + use windows::Win32::{Foundation::*, Graphics::Gdi::*, UI::WindowsAndMessaging::*}; + + const SZ: i32 = 32; + + unsafe fn new_argb_bitmap() -> (HBITMAP, *mut u32) { + let mut bits: *mut core::ffi::c_void = std::ptr::null_mut(); + let hdc = GetDC(HWND(std::ptr::null_mut())); + let bmi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: core::mem::size_of::() as u32, + biWidth: SZ, + biHeight: -SZ, + biPlanes: 1, + biBitCount: 32, + biCompression: BI_RGB.0, + ..Default::default() + }, + ..Default::default() + }; + let hbm = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, &mut bits, None, 0) + .expect("CreateDIBSection"); + ReleaseDC(HWND(std::ptr::null_mut()), hdc); + (hbm, bits.cast::()) + } + + unsafe fn new_and_mask() -> HBITMAP { + CreateBitmap(SZ, SZ, 1, 1, None) + } + + fn inside(px: f32, py: f32, poly: &[(i32, i32)]) -> bool { + let mut inside = false; + let mut j = poly.len() - 1; + for (i, &(xi, yi)) in poly.iter().enumerate() { + let (xj, yj) = (poly[j].0, poly[j].1); + if ((yi > py as i32) != (yj > py as i32)) + && (px < (xj - xi) as f32 * (py - yi as f32) / (yj - yi) as f32 + xi as f32) + { + inside = !inside; + } + j = i; + } + inside + } + + unsafe fn rasterise_polys(bits: *mut u32, polys: &[&[(i32, i32)]]) { + for y in 0..SZ { + for x in 0..SZ { + let opaque = polys + .iter() + .any(|poly| inside(x as f32 + 0.5, y as f32 + 0.5, poly)); + let p = bits.add((y * SZ + x) as usize); + if opaque { + *p = 0xFFFFFFFF; + } else { + *p = 0x00000000; + } + } + } + } + + unsafe fn icon_from_polys(polys: &[&[(i32, i32)]]) -> HICON { + let (hbm_color, bits) = new_argb_bitmap(); + rasterise_polys(bits, polys); + + let hbm_mask = new_and_mask(); + let icon_info = ICONINFO { + fIcon: TRUE, + xHotspot: 0, + yHotspot: 0, + hbmMask: hbm_mask, + hbmColor: hbm_color, + }; + let hicon = CreateIconIndirect(&icon_info).expect("CreateIconIndirect"); + + let _ = DeleteObject(hbm_mask); + let _ = DeleteObject(hbm_color); + hicon + } + + pub fn create_play_icon() -> HICON { + unsafe { icon_from_polys(&[&[(22, 16), (10, 7), (10, 25)]]) } + } + + pub fn create_pause_icon() -> HICON { + let bar_l = &[(11, 7), (14, 7), (14, 25), (11, 25)]; + let bar_r = &[(18, 7), (21, 7), (21, 25), (18, 25)]; + unsafe { icon_from_polys(&[bar_l, bar_r]) } + } + + pub fn create_next_icon() -> HICON { + let bar = &[(6, 7), (10, 7), (10, 25), (6, 25)]; + let tri = &[(20, 16), (12, 7), (12, 25)]; + unsafe { icon_from_polys(&[bar, tri]) } + } + + pub fn create_previous_icon() -> HICON { + let tri = &[(12, 16), (20, 7), (20, 25)]; + let bar = &[(22, 7), (26, 7), (26, 25), (22, 25)]; + unsafe { icon_from_polys(&[tri, bar]) } + } +}