diff --git a/src/stremio_app/app.rs b/src/stremio_app/app.rs index 024272e..09899d1 100644 --- a/src/stremio_app/app.rs +++ b/src/stremio_app/app.rs @@ -3,7 +3,7 @@ use native_windows_gui as nwg; use rand::Rng; use serde_json; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, io::Read, os::windows::process::CommandExt, path::{Path, PathBuf}, @@ -31,6 +31,7 @@ use super::stremio_server::StremioServer; #[derive(Default, NwgUi)] pub struct MainWindow { + pub pip_mode: Cell, pub command: String, pub commands_path: Option, pub webui_url: String, @@ -63,6 +64,7 @@ pub struct MainWindow { (tray_exit, OnMenuItemSelected): [nwg::stop_thread_dispatch()], (tray_show_hide, OnMenuItemSelected): [Self::on_show_hide], (tray_topmost, OnMenuItemSelected): [Self::on_toggle_topmost], + (tray_pip, OnMenuItemSelected): [Self::on_toggle_pip_notice], )] pub tray: SystemTray, #[nwg_partial(parent: window)] @@ -77,6 +79,9 @@ pub struct MainWindow { #[nwg_events(OnNotice: [Self::on_toggle_fullscreen_notice] )] pub toggle_fullscreen_notice: nwg::Notice, #[nwg_control] + #[nwg_events(OnNotice: [Self::on_toggle_pip_notice] )] + pub toggle_pip_notice: nwg::Notice, + #[nwg_control] #[nwg_events(OnNotice: [nwg::stop_thread_dispatch()] )] pub quit_notice: nwg::Notice, #[nwg_control] @@ -243,6 +248,7 @@ impl MainWindow { }); // thread let toggle_fullscreen_sender = self.toggle_fullscreen_notice.sender(); + let toggle_pip_sender = self.toggle_pip_notice.sender(); let quit_sender = self.quit_notice.sender(); let hide_splash_sender = self.hide_splash_notice.sender(); let focus_sender = self.focus_notice.sender(); @@ -259,6 +265,7 @@ impl MainWindow { web_tx_web.send(RPCResponse::get_handshake()).ok(); } Some("win-set-visibility") => toggle_fullscreen_sender.notice(), + Some("win-pip-toggle") => toggle_pip_sender.notice(), Some("quit") => quit_sender.notice(), Some("app-ready") => { hide_splash_sender.notice(); @@ -353,6 +360,10 @@ impl MainWindow { } fn on_min_max(&self, data: &nwg::EventData) { let data = data.on_min_max(); + if self.pip_mode.get() { + data.set_min_size(320, 180); + return; + } data.set_min_size(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT); } fn on_paint(&self) { @@ -372,6 +383,16 @@ impl MainWindow { } self.transmit_window_visibility_change(); } + fn on_toggle_pip_notice(&self) { + if let Some(hwnd) = self.window.handle.hwnd() { + if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() { + self.pip_mode.set(!saved_style.pip); + saved_style.toggle_pip(hwnd); + self.tray.tray_pip.set_checked(saved_style.pip); + self.webview.fit_to_window(self.window.handle.hwnd()); + } + } + } fn on_hide_splash_notice(&self) { self.splash_screen.hide(); } @@ -426,4 +447,4 @@ impl MainWindow { self.tray.tray_show_hide.set_checked(self.window.visible()); self.transmit_window_visibility_change(); } -} +} \ No newline at end of file diff --git a/src/stremio_app/systray.rs b/src/stremio_app/systray.rs index 8063fac..92e98b1 100644 --- a/src/stremio_app/systray.rs +++ b/src/stremio_app/systray.rs @@ -1,28 +1,30 @@ -use native_windows_derive::NwgPartial; -use native_windows_gui as nwg; - -#[derive(Default, NwgPartial)] -pub struct SystemTray { - #[nwg_resource] - pub embed: nwg::EmbedResource, - #[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))] - pub tray_icon: nwg::Icon, - #[nwg_control(icon: Some(&data.tray_icon), tip: Some("Stremio"))] - #[nwg_events(OnContextMenu: [Self::show_menu])] - pub tray: nwg::TrayNotification, - #[nwg_control(popup: true)] - pub tray_menu: nwg::Menu, - #[nwg_control(parent: tray_menu, text: "&Show window")] - pub tray_show_hide: nwg::MenuItem, - #[nwg_control(parent: tray_menu, text: "Always on &top")] - pub tray_topmost: nwg::MenuItem, - #[nwg_control(parent: tray_menu, text: "&Quit")] - pub tray_exit: nwg::MenuItem, -} - -impl SystemTray { - fn show_menu(&self) { - let (x, y) = nwg::GlobalCursor::position(); - self.tray_menu.popup(x, y); - } -} +use native_windows_derive::NwgPartial; +use native_windows_gui as nwg; + +#[derive(Default, NwgPartial)] +pub struct SystemTray { + #[nwg_resource] + pub embed: nwg::EmbedResource, + #[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))] + pub tray_icon: nwg::Icon, + #[nwg_control(icon: Some(&data.tray_icon), tip: Some("Stremio"))] + #[nwg_events(OnContextMenu: [Self::show_menu])] + pub tray: nwg::TrayNotification, + #[nwg_control(popup: true)] + pub tray_menu: nwg::Menu, + #[nwg_control(parent: tray_menu, text: "&Show window")] + pub tray_show_hide: nwg::MenuItem, + #[nwg_control(parent: tray_menu, text: "Always on &top")] + pub tray_topmost: nwg::MenuItem, + #[nwg_control(parent: tray_menu, text: "&Picture in Picture")] + pub tray_pip: nwg::MenuItem, + #[nwg_control(parent: tray_menu, text: "&Quit")] + pub tray_exit: nwg::MenuItem, +} + +impl SystemTray { + fn show_menu(&self) { + let (x, y) = nwg::GlobalCursor::position(); + self.tray_menu.popup(x, y); + } +} \ No newline at end of file diff --git a/src/stremio_app/window_helper.rs b/src/stremio_app/window_helper.rs index 68c5547..51b5f13 100644 --- a/src/stremio_app/window_helper.rs +++ b/src/stremio_app/window_helper.rs @@ -21,6 +21,9 @@ bitflags! { #[derive(Default, Clone)] pub struct WindowStyle { pub full_screen: bool, + pub pip: bool, + pub pip_pos: (i32, i32), + pub pip_size: (i32, i32), pub pos: (i32, i32), pub size: (i32, i32), pub style: i32, @@ -148,9 +151,70 @@ impl WindowStyle { } self.ex_style = unsafe { GetWindowLongA(hwnd, GWL_EXSTYLE) }; } + pub fn toggle_pip(&mut self, hwnd: HWND) { + if self.pip { + // Restore from PiP + unsafe { + SetWindowLongA(hwnd, GWL_STYLE, self.style); + SetWindowLongA(hwnd, GWL_EXSTYLE, self.ex_style); + } + let topmost = if self.ex_style as u32 & WS_EX_TOPMOST == WS_EX_TOPMOST { + HWND_TOPMOST + } else { + HWND_NOTOPMOST + }; + self.show_window_at(hwnd, topmost); + self.pip = false; + } else { + // Enter PiP — save current state + unsafe { + let mut rect = mem::zeroed(); + GetWindowRect(hwnd, &mut rect); + self.pos = (rect.left, rect.top); + self.size = ((rect.right - rect.left), (rect.bottom - rect.top)); + self.style = GetWindowLongA(hwnd, GWL_STYLE); + self.ex_style = GetWindowLongA(hwnd, GWL_EXSTYLE); + } + + // Small window, no caption, keep thick frame for resize, always on top + let pip_style = self.style & !(WS_THICKFRAME as i32); + let pip_ex_style = self.ex_style + & !(WS_EX_DLGMODALFRAME as i32 + | WS_EX_WINDOWEDGE as i32 + | WS_EX_CLIENTEDGE as i32 + | WS_EX_STATICEDGE as i32) + | WS_EX_TOPMOST as i32; + + unsafe { + SetWindowLongA(hwnd, GWL_STYLE, pip_style); + SetWindowLongA(hwnd, GWL_EXSTYLE, pip_ex_style); + } + + // Position bottom-right corner, 400x225 (16:9) + let monitor_w = unsafe { GetSystemMetrics(SM_CXSCREEN) }; + let monitor_h = unsafe { GetSystemMetrics(SM_CYSCREEN) }; + let pip_w = 400; + let pip_h = 225; + let pip_x = monitor_w - pip_w - 20; + let pip_y = monitor_h - pip_h - 60; + + unsafe { + SetWindowPos( + hwnd, + HWND_TOPMOST, + pip_x, + pip_y, + pip_w, + pip_h, + SWP_FRAMECHANGED, + ); + } + self.pip = true; + } + } pub fn set_active(&mut self, hwnd: HWND) { unsafe { SetForegroundWindow(hwnd); } } -} +} \ No newline at end of file