mirror of
https://github.com/Stremio/stremio-shell-ng.git
synced 2026-05-18 15:11:53 +00:00
Merge pull request #60 from Stremio/fix/window-state-titlebar-color
Some checks failed
Continuous integration / test (push) Has been cancelled
Some checks failed
Continuous integration / test (push) Has been cancelled
Fix: Window state and title bar color
This commit is contained in:
commit
b644519919
6 changed files with 266 additions and 9 deletions
|
|
@ -13,6 +13,7 @@ native-windows-gui = { git = "https://github.com/Stremio/native-windows-gui", fe
|
||||||
] }
|
] }
|
||||||
native-windows-derive = "1"
|
native-windows-derive = "1"
|
||||||
winapi = { version = "0.3.9", features = [
|
winapi = { version = "0.3.9", features = [
|
||||||
|
"dwmapi",
|
||||||
"libloaderapi",
|
"libloaderapi",
|
||||||
"handleapi",
|
"handleapi",
|
||||||
"jobapi2",
|
"jobapi2",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ use crate::stremio_app::{
|
||||||
systray::SystemTray,
|
systray::SystemTray,
|
||||||
updater,
|
updater,
|
||||||
window_helper::WindowStyle,
|
window_helper::WindowStyle,
|
||||||
|
window_settings::WindowSettings,
|
||||||
PipeServer,
|
PipeServer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -54,14 +55,15 @@ pub struct MainWindow {
|
||||||
OnPaint: [Self::on_paint],
|
OnPaint: [Self::on_paint],
|
||||||
OnMinMaxInfo: [Self::on_min_max(SELF, EVT_DATA)],
|
OnMinMaxInfo: [Self::on_min_max(SELF, EVT_DATA)],
|
||||||
OnWindowMinimize: [Self::transmit_window_state_change],
|
OnWindowMinimize: [Self::transmit_window_state_change],
|
||||||
OnWindowMaximize: [Self::transmit_window_state_change],
|
OnWindowMaximize: [Self::on_window_state_changed],
|
||||||
OnWindowFocus: [Self::transmit_window_state_change],
|
OnWindowFocus: [Self::transmit_window_state_change],
|
||||||
|
OnResizeEnd: [Self::save_window_settings],
|
||||||
)]
|
)]
|
||||||
pub window: nwg::Window,
|
pub window: nwg::Window,
|
||||||
#[nwg_partial(parent: window)]
|
#[nwg_partial(parent: window)]
|
||||||
#[nwg_events(
|
#[nwg_events(
|
||||||
(tray, MousePressLeftUp): [Self::on_show],
|
(tray, MousePressLeftUp): [Self::on_show],
|
||||||
(tray_exit, OnMenuItemSelected): [nwg::stop_thread_dispatch()],
|
(tray_exit, OnMenuItemSelected): [Self::on_exit],
|
||||||
(tray_show_hide, OnMenuItemSelected): [Self::on_show_hide],
|
(tray_show_hide, OnMenuItemSelected): [Self::on_show_hide],
|
||||||
(tray_topmost, OnMenuItemSelected): [Self::on_toggle_topmost],
|
(tray_topmost, OnMenuItemSelected): [Self::on_toggle_topmost],
|
||||||
)]
|
)]
|
||||||
|
|
@ -131,7 +133,13 @@ impl MainWindow {
|
||||||
self.webview.dev_tools.set(self.dev_tools).ok();
|
self.webview.dev_tools.set(self.dev_tools).ok();
|
||||||
if let Some(hwnd) = self.window.handle.hwnd() {
|
if let Some(hwnd) = self.window.handle.hwnd() {
|
||||||
if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() {
|
if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() {
|
||||||
saved_style.center_window(hwnd, WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT);
|
saved_style.set_title_bar_color(hwnd);
|
||||||
|
if let Some(window_settings) = WindowSettings::load() {
|
||||||
|
saved_style
|
||||||
|
.restore_window_placement(hwnd, window_settings.to_window_placement());
|
||||||
|
} else {
|
||||||
|
saved_style.center_window(hwnd, WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -417,6 +425,25 @@ impl MainWindow {
|
||||||
self.webview.fit_to_window(self.window.handle.hwnd());
|
self.webview.fit_to_window(self.window.handle.hwnd());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn on_window_state_changed(&self) {
|
||||||
|
self.save_window_settings();
|
||||||
|
self.transmit_window_state_change();
|
||||||
|
}
|
||||||
|
fn save_window_settings(&self) {
|
||||||
|
if self
|
||||||
|
.saved_window_style
|
||||||
|
.try_borrow()
|
||||||
|
.map(|style| style.full_screen)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(hwnd) = self.window.handle.hwnd() {
|
||||||
|
if let Err(err) = WindowSettings::save(hwnd) {
|
||||||
|
eprintln!("Cannot save window settings: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fn on_toggle_fullscreen_notice(&self) {
|
fn on_toggle_fullscreen_notice(&self) {
|
||||||
if let Some(hwnd) = self.window.handle.hwnd() {
|
if let Some(hwnd) = self.window.handle.hwnd() {
|
||||||
if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() {
|
if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() {
|
||||||
|
|
@ -485,8 +512,13 @@ impl MainWindow {
|
||||||
if let nwg::EventData::OnWindowClose(data) = data {
|
if let nwg::EventData::OnWindowClose(data) = data {
|
||||||
data.close(false);
|
data.close(false);
|
||||||
}
|
}
|
||||||
|
self.save_window_settings();
|
||||||
self.window.set_visible(false);
|
self.window.set_visible(false);
|
||||||
self.tray.tray_show_hide.set_checked(self.window.visible());
|
self.tray.tray_show_hide.set_checked(self.window.visible());
|
||||||
self.transmit_window_visibility_change();
|
self.transmit_window_visibility_change();
|
||||||
}
|
}
|
||||||
|
fn on_exit(&self) {
|
||||||
|
self.save_window_settings();
|
||||||
|
nwg::stop_thread_dispatch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ pub mod named_pipe;
|
||||||
pub mod splash;
|
pub mod splash;
|
||||||
pub mod systray;
|
pub mod systray;
|
||||||
pub mod window_helper;
|
pub mod window_helper;
|
||||||
|
pub mod window_settings;
|
||||||
pub use named_pipe::{PipeClient, PipeServer};
|
pub use named_pipe::{PipeClient, PipeServer};
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod updater;
|
pub mod updater;
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ pub enum StrProp {
|
||||||
SubBorderColor,
|
SubBorderColor,
|
||||||
SubColor,
|
SubColor,
|
||||||
TrackList,
|
TrackList,
|
||||||
Vf,
|
Vf,
|
||||||
VideoParams,
|
VideoParams,
|
||||||
Vo,
|
Vo,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,26 @@
|
||||||
use std::{cmp, mem};
|
use std::{cmp, mem};
|
||||||
|
use winapi::ctypes::c_void;
|
||||||
|
use winapi::shared::minwindef::DWORD;
|
||||||
use winapi::shared::windef::HWND;
|
use winapi::shared::windef::HWND;
|
||||||
|
use winapi::um::dwmapi::DwmSetWindowAttribute;
|
||||||
use winapi::um::winuser::{
|
use winapi::um::winuser::{
|
||||||
GetForegroundWindow, GetMonitorInfoA, GetSystemMetrics, GetWindowLongA, GetWindowRect,
|
GetForegroundWindow, GetMonitorInfoA, GetSystemMetrics, GetWindowLongA, GetWindowRect,
|
||||||
IsIconic, IsZoomed, MonitorFromWindow, SetForegroundWindow, SetWindowLongA, SetWindowPos,
|
IsIconic, IsZoomed, MonitorFromWindow, SetForegroundWindow, SetWindowLongA, SetWindowPlacement,
|
||||||
GWL_EXSTYLE, GWL_STYLE, HWND_NOTOPMOST, HWND_TOPMOST, MONITORINFO, MONITOR_DEFAULTTONEAREST,
|
SetWindowPos, GWL_EXSTYLE, GWL_STYLE, HWND_NOTOPMOST, HWND_TOPMOST, MONITORINFO,
|
||||||
SM_CXSCREEN, SM_CYSCREEN, SWP_FRAMECHANGED, SWP_NOMOVE, SWP_NOSIZE, WS_CAPTION,
|
MONITOR_DEFAULTTONEAREST, SM_CXSCREEN, SM_CYSCREEN, SWP_FRAMECHANGED, SWP_NOMOVE, SWP_NOSIZE,
|
||||||
WS_EX_CLIENTEDGE, WS_EX_DLGMODALFRAME, WS_EX_STATICEDGE, WS_EX_TOPMOST, WS_EX_WINDOWEDGE,
|
WINDOWPLACEMENT, WS_CAPTION, WS_EX_CLIENTEDGE, WS_EX_DLGMODALFRAME, WS_EX_STATICEDGE,
|
||||||
WS_THICKFRAME,
|
WS_EX_TOPMOST, WS_EX_WINDOWEDGE, WS_THICKFRAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DWMWA_CAPTION_COLOR: DWORD = 35;
|
||||||
|
const DWMWA_TEXT_COLOR: DWORD = 36;
|
||||||
|
const STREMIO_CAPTION_COLOR: DWORD = colorref(0x15, 0x12, 0x2b);
|
||||||
|
const WHITE_TEXT_COLOR: DWORD = colorref(0xff, 0xff, 0xff);
|
||||||
|
|
||||||
|
const fn colorref(red: DWORD, green: DWORD, blue: DWORD) -> DWORD {
|
||||||
|
red | (green << 8) | (blue << 16)
|
||||||
|
}
|
||||||
|
|
||||||
// https://doc.qt.io/qt-5/qt.html#WindowState-enum
|
// https://doc.qt.io/qt-5/qt.html#WindowState-enum
|
||||||
bitflags! {
|
bitflags! {
|
||||||
struct WindowState: u8 {
|
struct WindowState: u8 {
|
||||||
|
|
@ -71,6 +84,35 @@ impl WindowStyle {
|
||||||
self.pos = ((monitor_w - self.size.0) / 2, (monitor_h - self.size.1) / 2);
|
self.pos = ((monitor_w - self.size.0) / 2, (monitor_h - self.size.1) / 2);
|
||||||
self.show_window_at(hwnd, HWND_NOTOPMOST);
|
self.show_window_at(hwnd, HWND_NOTOPMOST);
|
||||||
}
|
}
|
||||||
|
pub fn restore_window_placement(&mut self, hwnd: HWND, placement: WINDOWPLACEMENT) {
|
||||||
|
self.pos = (
|
||||||
|
placement.rcNormalPosition.left,
|
||||||
|
placement.rcNormalPosition.top,
|
||||||
|
);
|
||||||
|
self.size = (
|
||||||
|
placement.rcNormalPosition.right - placement.rcNormalPosition.left,
|
||||||
|
placement.rcNormalPosition.bottom - placement.rcNormalPosition.top,
|
||||||
|
);
|
||||||
|
unsafe {
|
||||||
|
SetWindowPlacement(hwnd, &placement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_title_bar_color(&self, hwnd: HWND) {
|
||||||
|
unsafe {
|
||||||
|
DwmSetWindowAttribute(
|
||||||
|
hwnd,
|
||||||
|
DWMWA_CAPTION_COLOR,
|
||||||
|
&STREMIO_CAPTION_COLOR as *const _ as *const c_void,
|
||||||
|
mem::size_of_val(&STREMIO_CAPTION_COLOR) as DWORD,
|
||||||
|
);
|
||||||
|
DwmSetWindowAttribute(
|
||||||
|
hwnd,
|
||||||
|
DWMWA_TEXT_COLOR,
|
||||||
|
&WHITE_TEXT_COLOR as *const _ as *const c_void,
|
||||||
|
mem::size_of_val(&WHITE_TEXT_COLOR) as DWORD,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn set_full_screen(&mut self, hwnd: HWND, full_screen: bool) {
|
pub fn set_full_screen(&mut self, hwnd: HWND, full_screen: bool) {
|
||||||
if self.full_screen == full_screen {
|
if self.full_screen == full_screen {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
181
src/stremio_app/window_settings.rs
Normal file
181
src/stremio_app/window_settings.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{env, fs, io, path::PathBuf};
|
||||||
|
use winapi::shared::windef::{HWND, POINT, RECT};
|
||||||
|
use winapi::um::winuser::{
|
||||||
|
GetWindowPlacement, IsIconic, SW_SHOWMAXIMIZED, SW_SHOWNORMAL, WINDOWPLACEMENT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const WINDOW_SETTINGS_FILE: &str = "window-state.json";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct WindowSettings {
|
||||||
|
show_cmd: u32,
|
||||||
|
min_position: Point,
|
||||||
|
max_position: Point,
|
||||||
|
normal_position: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
struct Point {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
struct Rect {
|
||||||
|
left: i32,
|
||||||
|
top: i32,
|
||||||
|
right: i32,
|
||||||
|
bottom: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowSettings {
|
||||||
|
pub fn load() -> Option<Self> {
|
||||||
|
fs::read_to_string(settings_path())
|
||||||
|
.ok()
|
||||||
|
.and_then(|settings| serde_json::from_str(&settings).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(hwnd: HWND) -> io::Result<()> {
|
||||||
|
let Some(settings) = Self::from_window(hwnd) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let path = settings_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let json = serde_json::to_string_pretty(&settings).map_err(io::Error::other)?;
|
||||||
|
fs::write(path, json)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_window_placement(&self) -> WINDOWPLACEMENT {
|
||||||
|
let mut placement = WINDOWPLACEMENT {
|
||||||
|
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
|
||||||
|
flags: 0,
|
||||||
|
showCmd: self.show_cmd,
|
||||||
|
ptMinPosition: self.min_position.clone().into(),
|
||||||
|
ptMaxPosition: self.max_position.clone().into(),
|
||||||
|
rcNormalPosition: self.normal_position.clone().into(),
|
||||||
|
};
|
||||||
|
if !is_restorable_size(&placement.rcNormalPosition) {
|
||||||
|
placement.showCmd = SW_SHOWNORMAL as u32;
|
||||||
|
}
|
||||||
|
placement
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_window(hwnd: HWND) -> Option<Self> {
|
||||||
|
if unsafe { IsIconic(hwnd) } != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut placement = WINDOWPLACEMENT {
|
||||||
|
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
|
||||||
|
flags: 0,
|
||||||
|
showCmd: 0,
|
||||||
|
ptMinPosition: POINT { x: 0, y: 0 },
|
||||||
|
ptMaxPosition: POINT { x: 0, y: 0 },
|
||||||
|
rcNormalPosition: RECT {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if unsafe { GetWindowPlacement(hwnd, &mut placement) } == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !is_restorable_size(&placement.rcNormalPosition) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(WindowSettings {
|
||||||
|
show_cmd: if placement.showCmd == SW_SHOWMAXIMIZED as u32 {
|
||||||
|
SW_SHOWMAXIMIZED as u32
|
||||||
|
} else {
|
||||||
|
SW_SHOWNORMAL as u32
|
||||||
|
},
|
||||||
|
min_position: placement.ptMinPosition.into(),
|
||||||
|
max_position: placement.ptMaxPosition.into(),
|
||||||
|
normal_position: placement.rcNormalPosition.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn settings_path() -> PathBuf {
|
||||||
|
env::var_os("APPDATA")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(env::temp_dir)
|
||||||
|
.join("Stremio")
|
||||||
|
.join(WINDOW_SETTINGS_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_restorable_size(rect: &RECT) -> bool {
|
||||||
|
rect.right > rect.left && rect.bottom > rect.top
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<POINT> for Point {
|
||||||
|
fn from(point: POINT) -> Self {
|
||||||
|
Point {
|
||||||
|
x: point.x,
|
||||||
|
y: point.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Point> for POINT {
|
||||||
|
fn from(point: Point) -> Self {
|
||||||
|
POINT {
|
||||||
|
x: point.x,
|
||||||
|
y: point.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RECT> for Rect {
|
||||||
|
fn from(rect: RECT) -> Self {
|
||||||
|
Rect {
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
right: rect.right,
|
||||||
|
bottom: rect.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Rect> for RECT {
|
||||||
|
fn from(rect: Rect) -> Self {
|
||||||
|
RECT {
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
right: rect.right,
|
||||||
|
bottom: rect.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::is_restorable_size;
|
||||||
|
use winapi::shared::windef::RECT;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_empty_window_rect() {
|
||||||
|
assert!(!is_restorable_size(&RECT {
|
||||||
|
left: 10,
|
||||||
|
top: 10,
|
||||||
|
right: 10,
|
||||||
|
bottom: 20,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_non_empty_window_rect() {
|
||||||
|
assert!(is_restorable_size(&RECT {
|
||||||
|
left: 10,
|
||||||
|
top: 10,
|
||||||
|
right: 20,
|
||||||
|
bottom: 20,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue