mirror of
https://github.com/Stremio/stremio-shell-ng.git
synced 2026-05-11 20:50:36 +00:00
fix: window state and title bar color
Some checks are pending
Continuous integration / test (push) Waiting to run
Some checks are pending
Continuous integration / test (push) Waiting to run
This commit is contained in:
parent
bbbe882faf
commit
b06ce07077
5 changed files with 260 additions and 8 deletions
|
|
@ -13,6 +13,7 @@ native-windows-gui = { git = "https://github.com/Stremio/native-windows-gui", fe
|
|||
] }
|
||||
native-windows-derive = "1"
|
||||
winapi = { version = "0.3.9", features = [
|
||||
"dwmapi",
|
||||
"libloaderapi",
|
||||
"handleapi",
|
||||
"jobapi2",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ use crate::stremio_app::{
|
|||
systray::SystemTray,
|
||||
updater,
|
||||
window_helper::WindowStyle,
|
||||
window_settings::WindowSettings,
|
||||
PipeServer,
|
||||
};
|
||||
|
||||
|
|
@ -53,14 +54,15 @@ pub struct MainWindow {
|
|||
OnPaint: [Self::on_paint],
|
||||
OnMinMaxInfo: [Self::on_min_max(SELF, EVT_DATA)],
|
||||
OnWindowMinimize: [Self::transmit_window_state_change],
|
||||
OnWindowMaximize: [Self::transmit_window_state_change],
|
||||
OnWindowMaximize: [Self::on_window_state_changed],
|
||||
OnWindowFocus: [Self::transmit_window_state_change],
|
||||
OnResizeEnd: [Self::save_window_settings],
|
||||
)]
|
||||
pub window: nwg::Window,
|
||||
#[nwg_partial(parent: window)]
|
||||
#[nwg_events(
|
||||
(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_topmost, OnMenuItemSelected): [Self::on_toggle_topmost],
|
||||
)]
|
||||
|
|
@ -130,7 +132,13 @@ impl MainWindow {
|
|||
self.webview.dev_tools.set(self.dev_tools).ok();
|
||||
if let Some(hwnd) = self.window.handle.hwnd() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -360,6 +368,25 @@ impl MainWindow {
|
|||
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) {
|
||||
if let Some(hwnd) = self.window.handle.hwnd() {
|
||||
if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() {
|
||||
|
|
@ -422,8 +449,13 @@ impl MainWindow {
|
|||
if let nwg::EventData::OnWindowClose(data) = data {
|
||||
data.close(false);
|
||||
}
|
||||
self.save_window_settings();
|
||||
self.window.set_visible(false);
|
||||
self.tray.tray_show_hide.set_checked(self.window.visible());
|
||||
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 systray;
|
||||
pub mod window_helper;
|
||||
pub mod window_settings;
|
||||
pub use named_pipe::{PipeClient, PipeServer};
|
||||
pub mod constants;
|
||||
pub mod updater;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
use std::{cmp, mem};
|
||||
use winapi::ctypes::c_void;
|
||||
use winapi::shared::minwindef::DWORD;
|
||||
use winapi::shared::windef::HWND;
|
||||
use winapi::um::dwmapi::DwmSetWindowAttribute;
|
||||
use winapi::um::winuser::{
|
||||
GetForegroundWindow, GetMonitorInfoA, GetSystemMetrics, GetWindowLongA, GetWindowRect,
|
||||
IsIconic, IsZoomed, MonitorFromWindow, SetForegroundWindow, SetWindowLongA, SetWindowPos,
|
||||
GWL_EXSTYLE, GWL_STYLE, HWND_NOTOPMOST, HWND_TOPMOST, MONITORINFO, MONITOR_DEFAULTTONEAREST,
|
||||
SM_CXSCREEN, SM_CYSCREEN, SWP_FRAMECHANGED, SWP_NOMOVE, SWP_NOSIZE, WS_CAPTION,
|
||||
WS_EX_CLIENTEDGE, WS_EX_DLGMODALFRAME, WS_EX_STATICEDGE, WS_EX_TOPMOST, WS_EX_WINDOWEDGE,
|
||||
WS_THICKFRAME,
|
||||
IsIconic, IsZoomed, MonitorFromWindow, SetForegroundWindow, SetWindowLongA, SetWindowPlacement,
|
||||
SetWindowPos, GWL_EXSTYLE, GWL_STYLE, HWND_NOTOPMOST, HWND_TOPMOST, MONITORINFO,
|
||||
MONITOR_DEFAULTTONEAREST, SM_CXSCREEN, SM_CYSCREEN, SWP_FRAMECHANGED, SWP_NOMOVE, SWP_NOSIZE,
|
||||
WINDOWPLACEMENT, WS_CAPTION, WS_EX_CLIENTEDGE, WS_EX_DLGMODALFRAME, WS_EX_STATICEDGE,
|
||||
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 = 0x00f72f7b;
|
||||
const WHITE_TEXT_COLOR: DWORD = 0x00ffffff;
|
||||
// https://doc.qt.io/qt-5/qt.html#WindowState-enum
|
||||
bitflags! {
|
||||
struct WindowState: u8 {
|
||||
|
|
@ -71,6 +79,35 @@ impl WindowStyle {
|
|||
self.pos = ((monitor_w - self.size.0) / 2, (monitor_h - self.size.1) / 2);
|
||||
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 toggle_full_screen(&mut self, hwnd: HWND) {
|
||||
if self.full_screen {
|
||||
let topmost = if self.ex_style as u32 & WS_EX_TOPMOST == WS_EX_TOPMOST {
|
||||
|
|
|
|||
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