diff --git a/Cargo.lock b/Cargo.lock index c2646dc..1533a2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1310,6 +1310,8 @@ dependencies = [ "serde", "toml", "ureq", + "windows 0.43.0", + "winres", ] [[package]] @@ -2928,6 +2930,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml", +] + [[package]] name = "wio" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 7459105..1747cb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,10 @@ name = "ktu-timetable" version = "0.1.0" edition = "2021" +build = "build.rs" + +[build-dependencies] +winres = "0.1" [profile.release] opt-level = 'z' # Optimize for size @@ -22,4 +26,14 @@ lazy-regex = "2.4.1" directories-next = "2.0.0" toml = "0.5.11" serde = { version = "1.0.152", features = ["derive"]} -lazy_static = "1.4.0" \ No newline at end of file +lazy_static = "1.4.0" +winres = "0.1.12" + +[dependencies.windows] +version = "0.43.0" +features = [ + "Win32_UI_WindowsAndMessaging", + "Win32_Foundation", + "Win32_System_LibraryLoader", + "Win32_Graphics_Gdi" +] \ No newline at end of file diff --git a/assets/icon.ase b/assets/icon.ase new file mode 100644 index 0000000..49b04fd Binary files /dev/null and b/assets/icon.ase differ diff --git a/assets/icon.ico b/assets/icon.ico index 5e252b2..5593569 100644 Binary files a/assets/icon.ico and b/assets/icon.ico differ diff --git a/assets/icon.png b/assets/icon.png deleted file mode 100644 index 2256e9d..0000000 Binary files a/assets/icon.png and /dev/null differ diff --git a/assets/small-icon.ase b/assets/small-icon.ase new file mode 100644 index 0000000..7bb5012 Binary files /dev/null and b/assets/small-icon.ase differ diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..d83fa12 --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ +extern crate winres; + +fn main() { + if cfg!(target_os = "windows") { + let mut res = winres::WindowsResource::new(); + res.set_icon_with_id("assets/icon.ico", "window-icon"); + res.compile().unwrap(); + } +} \ No newline at end of file diff --git a/src/environment.rs b/src/environment.rs new file mode 100644 index 0000000..e47263a --- /dev/null +++ b/src/environment.rs @@ -0,0 +1,15 @@ +use crate::{timetable::{TimetableGetter, BlockingTimetableGetter}, config::{ConfigStore, TomlConfigStore}}; + +pub struct Environment { + pub timetable_getter: Box, + pub config_store: Box +} + +impl Default for Environment { + fn default() -> Self { + Self { + config_store: Box::new(TomlConfigStore::default()), + timetable_getter: Box::new(BlockingTimetableGetter::default()) + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 375131a..6cd4dde 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,13 +5,14 @@ mod app; mod config; mod events_table; mod utils; +mod platforms; +mod environment; #[macro_use] extern crate lazy_static; -use app::MainApp; use config::TomlConfigStore; -use eframe::egui; +use environment::Environment; use timetable::BlockingTimetableGetter; // TODO: show errors when loading config @@ -19,7 +20,7 @@ use timetable::BlockingTimetableGetter; // TODO: use "confy" for config loading? // TODO: Setup pipeline -fn main() -> Result<(), ureq::Error> { +fn main() { let config_store = TomlConfigStore::default(); // let config_store = MemoryConfigStore::new(Config { // vidko: None//Some("E1810".into()) @@ -41,29 +42,9 @@ fn main() -> Result<(), ureq::Error> { // ] // }); - let mut native_options = eframe::NativeOptions::default(); - native_options.decorated = true; - native_options.resizable = true; - native_options.min_window_size = Some(egui::vec2(480.0, 320.0)); - native_options.initial_window_size = Some(egui::vec2(500.0, 320.0)); - native_options.icon_data = Some(eframe::IconData { - rgba: image::load_from_memory(include_bytes!("../assets/icon.png")) - .expect("Failed to load icon") - .into_rgb8() - .into_raw(), - width: 32, - height: 32, - }); - let mut app = MainApp::new(Box::new(config_store), Box::new(timetable_getter)); - - eframe::run_native( - "KTU timetable", - native_options, - Box::new(move |cc| { - app.init(cc); - Box::new(app) - }) - ); - - Ok(()) + platforms::run_windows_app(Environment { + timetable_getter: Box::new(timetable_getter), + config_store: Box::new(config_store) + }) } + diff --git a/src/platforms/mod.rs b/src/platforms/mod.rs new file mode 100644 index 0000000..5983e1c --- /dev/null +++ b/src/platforms/mod.rs @@ -0,0 +1,3 @@ +mod win; + +pub use win::run_windows_app; \ No newline at end of file diff --git a/src/platforms/win.rs b/src/platforms/win.rs new file mode 100644 index 0000000..e307297 --- /dev/null +++ b/src/platforms/win.rs @@ -0,0 +1,132 @@ +use eframe::IconData; +use windows::{ + w, + Win32::{ + Graphics::Gdi::{ + CreateCompatibleDC, DeleteDC, GetDIBits, GetObjectA, SelectObject, BITMAP, BITMAPINFO, + BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, + }, + System::LibraryLoader::GetModuleHandleW, + UI::WindowsAndMessaging::{ + GetIconInfo, LoadImageW, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR, + }, + }, +}; + +use crate::{environment::Environment, app::MainApp}; + +// Yoinked from https://github.com/emilk/egui/issues/920#issuecomment-1364446538 +fn load_app_icon() -> IconData { + let (mut buffer, width, height) = unsafe { + let h_instance = GetModuleHandleW(None).expect("Failed to get HINSTANCE"); + let icon = LoadImageW( + h_instance, + w!("window-icon"), + IMAGE_ICON, + 64, + 64, + LR_DEFAULTCOLOR, + ) + .expect("Failed to load icon"); + + let mut icon_info = ICONINFO::default(); + let res = GetIconInfo(HICON(icon.0), &mut icon_info as *mut _).as_bool(); + if !res { + panic!("Failed to load icon info"); + } + + let mut bitmap = BITMAP::default(); + GetObjectA( + icon_info.hbmColor, + std::mem::size_of::() as i32, + Some(&mut bitmap as *mut _ as *mut _), + ); + + let width = bitmap.bmWidth; + let height = bitmap.bmHeight; + + let b_size = (width * height * 4) as usize; + let mut buffer = Vec::::with_capacity(b_size); + + let h_dc = CreateCompatibleDC(None); + let h_bitmap = SelectObject(h_dc, icon_info.hbmColor); + + let mut bitmap_info = BITMAPINFO::default(); + bitmap_info.bmiHeader.biSize = std::mem::size_of::() as u32; + bitmap_info.bmiHeader.biWidth = width; + bitmap_info.bmiHeader.biHeight = height; + bitmap_info.bmiHeader.biPlanes = 1; + bitmap_info.bmiHeader.biBitCount = 32; + bitmap_info.bmiHeader.biCompression = BI_RGB; + bitmap_info.bmiHeader.biSizeImage = 0; + + let res = GetDIBits( + h_dc, + icon_info.hbmColor, + 0, + height as u32, + Some(buffer.spare_capacity_mut().as_mut_ptr() as *mut _), + &mut bitmap_info as *mut _, + DIB_RGB_COLORS, + ); + if res == 0 { + panic!("Failed to get RGB DI bits"); + } + + SelectObject(h_dc, h_bitmap); + DeleteDC(h_dc); + + assert_eq!( + bitmap_info.bmiHeader.biSizeImage as usize, + b_size, + "returned biSizeImage must equal to b_size" + ); + + // set the new size + buffer.set_len(bitmap_info.bmiHeader.biSizeImage as usize); + + (buffer, width as u32, height as u32) + }; + + // RGBA -> BGRA + for pixel in buffer.as_mut_slice().chunks_mut(4) { + pixel.swap(0, 2); + } + + // Flip the image vertically + let row_size = width as usize * 4; // number of pixels in each row + let row_count = buffer.len() as usize / row_size; // number of rows in the image + for row in 0..row_count / 2 { + // loop through half of the rows + let start = row * row_size; // index of the start of the current row + let end = (row_count - row - 1) * row_size; // index of the end of the current row + for i in 0..row_size { + buffer.swap(start + i, end + i); + } + } + + IconData { + rgba: buffer, + width, + height, + } +} + +pub fn run_windows_app(env: Environment) { + let mut native_options = eframe::NativeOptions::default(); + native_options.decorated = true; + native_options.resizable = true; + native_options.min_window_size = Some(egui::vec2(480.0, 320.0)); + native_options.initial_window_size = Some(egui::vec2(500.0, 320.0)); + native_options.icon_data = Some(load_app_icon()); + let mut app = MainApp::new(env.config_store, env.timetable_getter); + + eframe::run_native( + "KTU timetable", + native_options, + Box::new(move |cc| { + app.init(cc); + Box::new(app) + }) + ); +} \ No newline at end of file