feat: icon for app, and windows specific build code
This commit is contained in:
parent
307daaea7b
commit
090bea5805
971
Cargo.lock
generated
971
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
Cargo.toml
24
Cargo.toml
@ -21,18 +21,16 @@ rodio = "0.18.1"
|
|||||||
serde = { version = "1.0.171", features = ["derive"] }
|
serde = { version = "1.0.171", features = ["derive"] }
|
||||||
tokio = { version = "1.29.1", features = ["full"] }
|
tokio = { version = "1.29.1", features = ["full"] }
|
||||||
xxhash-rust = { version = "0.8.6", features = ["xxh3", "const_xxh3"] }
|
xxhash-rust = { version = "0.8.6", features = ["xxh3", "const_xxh3"] }
|
||||||
strinto = { path = "./strinto" }
|
|
||||||
eframe = "0.27.2"
|
eframe = "0.27.2"
|
||||||
tray-item = "0.10"
|
tray-item = "0.10"
|
||||||
winit = "0.30"
|
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
tray-icon = "0.14.3"
|
|
||||||
raw-window-handle = "0.6.2"
|
raw-window-handle = "0.6.2"
|
||||||
|
image = "0.25.1"
|
||||||
|
|
||||||
[build-dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
embed-resource = "2.3"
|
winit = "0.30"
|
||||||
|
|
||||||
[dependencies.windows]
|
[target.'cfg(windows)'.dependencies.windows]
|
||||||
version = "0.57.0"
|
version = "0.57.0"
|
||||||
features = [
|
features = [
|
||||||
"Data_Xml_Dom",
|
"Data_Xml_Dom",
|
||||||
@ -40,4 +38,18 @@ features = [
|
|||||||
"Win32_Security",
|
"Win32_Security",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
|
||||||
|
"Win32_System_LibraryLoader",
|
||||||
|
"Win32_Graphics_Gdi",
|
||||||
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
|
|
||||||
|
"Win32_UI_Controls",
|
||||||
|
"Win32_Graphics_Dwm",
|
||||||
|
"Win32_UI_Shell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
#[target.'cfg(windows)'.dependencies.windows-sys]
|
||||||
|
#version = "0.52.0"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
|
embed-resource = "2.3"
|
||||||
|
3
build.rs
3
build.rs
@ -1,5 +1,8 @@
|
|||||||
|
#[cfg(windows)]
|
||||||
extern crate embed_resource;
|
extern crate embed_resource;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
embed_resource::compile("soundboard.rc", embed_resource::NONE);
|
embed_resource::compile("soundboard.rc", embed_resource::NONE);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
BIN
icons/soundboard.ico
Normal file
BIN
icons/soundboard.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 211 KiB |
@ -1 +1 @@
|
|||||||
icon-red ICON "icons/icon-red.ico"
|
icon-soundboard ICON "icons/soundboard.ico"
|
@ -1,68 +0,0 @@
|
|||||||
///
|
|
||||||
/// Trying to get around "".into() for String values.
|
|
||||||
/// Or "".to_owned().
|
|
||||||
/// Or String::from("").
|
|
||||||
/// Or "".to_string().
|
|
||||||
/// Choose your church.
|
|
||||||
///
|
|
||||||
/// This is as far as you will get declaratively.
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! strinto {
|
|
||||||
($struct:ident { $($field:ident : $value:expr),* $(,)? }) => {
|
|
||||||
$struct {
|
|
||||||
$(
|
|
||||||
$field: $crate::strinto!(@convert $value),
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
(@convert $value:literal) => {
|
|
||||||
match () {
|
|
||||||
_ if stringify!($value).starts_with("\"") => {
|
|
||||||
$value.to_string()
|
|
||||||
},
|
|
||||||
_ => $value.into(), // <-- no getting rid of the into!
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SomeStruct {
|
|
||||||
first_name: String,
|
|
||||||
//last_name: String, // NOPE because of @convert.
|
|
||||||
//age: usize, // reason of .into() in the first place.
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main() {
|
|
||||||
let x = strinto!(SomeStruct {
|
|
||||||
first_name: "First",
|
|
||||||
//last_name: String::from("Last"), // NOPE 2.
|
|
||||||
//age: 1, // NOPE 1. But I went further.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// while this compiles for only &str "", it is also useless compared to into!
|
|
||||||
// the reason is, that you cannot type check in the declarative macros.
|
|
||||||
// while yes, you would conditionally run stringify!($value) in the match, but the match expansion
|
|
||||||
// will always lead you to type errors if you don't do an .into() in the other arm.
|
|
||||||
// also in the end it fails to compile for anything but all members of the struct being Strings.
|
|
||||||
|
|
||||||
// the last idea was going with a helper trait, but that will bleed into the runtime code, and yet again only work on pure String structs.
|
|
||||||
|
|
||||||
// I guess I have to embrace String::from(), .to_string(), .to_owned(), .into() madness, for something every human reader can deduce in a second,
|
|
||||||
// if you would just implicitly convert &str to String if literally written in the code.
|
|
||||||
|
|
||||||
// It is kinda amusing, that the other solution, to use new() kind of turns me off, because I cannot be explicit about the parameter name in the call.
|
|
||||||
// I would literally love the option to be more explicit in function param names.
|
|
||||||
|
|
||||||
// while builder patterns feel a bit bloated for static runtime options, i will probably look into automations for that.
|
|
||||||
|
|
||||||
// this is probably the only aesthetic decision in rust I probably will hate forever.
|
|
||||||
// It does not make sense, Strings as literals already are special in your code.
|
|
||||||
// Because numbers are as well, you dont have to write 123.into() either.
|
|
||||||
// I know I probably made some really harsh logical mistakes in my opinion here, and maybe it can be proven, that I am wrong, and I would love to hear that
|
|
||||||
// However it kind of feels like an excuse to not simplify assigning declaratively written &str to Strings in the code.
|
|
||||||
|
|
||||||
// And it makes sense to be explicit about creating a String Buffer sometimes, but it does not make sense mostly.
|
|
||||||
|
|
||||||
// Anyway, I will still try a procedural macro for this, just for fun.
|
|
251
src/bin/ui_icon.rs
Normal file
251
src/bin/ui_icon.rs
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
use eframe::{egui, NativeOptions};
|
||||||
|
use std::{ffi::OsStr, os::windows::ffi::OsStrExt, ptr};
|
||||||
|
use windows::{
|
||||||
|
core::PCWSTR,
|
||||||
|
Win32::{
|
||||||
|
System::LibraryLoader::GetModuleHandleW,
|
||||||
|
UI::WindowsAndMessaging::{LoadImageW, IMAGE_ICON, LR_DEFAULTCOLOR},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://github.com/emilk/egui/issues/920
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Run the UI on the main thread
|
||||||
|
run_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_ui() {
|
||||||
|
let app = NativeApp {};
|
||||||
|
|
||||||
|
// Attempt to load the icon from resources
|
||||||
|
let icon_handle = match load_icon_from_resource("icon-soundboard") {
|
||||||
|
Ok(handle) => Some(handle as isize),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to load icon: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// load from icon resource, cross platform.
|
||||||
|
// let icon = load_icon_static();
|
||||||
|
// load from exe resource
|
||||||
|
let icon = from_windows::load_app_icon("icon-soundboard");
|
||||||
|
|
||||||
|
let native_options = NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default().with_icon(icon),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
eframe::run_native(
|
||||||
|
"Custom window frame", // unused title
|
||||||
|
native_options,
|
||||||
|
Box::new(|_cc| Box::new(app)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
struct NativeApp {}
|
||||||
|
|
||||||
|
impl eframe::App for NativeApp {
|
||||||
|
fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
ui.heading("Testing Software");
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Blah:");
|
||||||
|
});
|
||||||
|
|
||||||
|
if ui.button("Apply").clicked() {
|
||||||
|
eprintln!("Apply clicked");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_wstring(str: &str) -> Vec<u16> {
|
||||||
|
OsStr::new(str)
|
||||||
|
.encode_wide()
|
||||||
|
.chain(Some(0).into_iter())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_icon_from_resource(resource_name: &str) -> Result<isize, String> {
|
||||||
|
let icon = unsafe {
|
||||||
|
let hmodule = if let Ok(hmodule) = GetModuleHandleW(None) {
|
||||||
|
hmodule
|
||||||
|
} else {
|
||||||
|
return Err("Error getting windows module handle".to_owned());
|
||||||
|
};
|
||||||
|
let handle = if let Ok(handle) = LoadImageW(
|
||||||
|
hmodule,
|
||||||
|
PCWSTR(to_wstring(resource_name).as_ptr()),
|
||||||
|
IMAGE_ICON,
|
||||||
|
64,
|
||||||
|
64,
|
||||||
|
LR_DEFAULTCOLOR,
|
||||||
|
) {
|
||||||
|
handle
|
||||||
|
} else {
|
||||||
|
return Err("Error getting image handle".to_owned());
|
||||||
|
};
|
||||||
|
|
||||||
|
handle
|
||||||
|
};
|
||||||
|
Ok(icon.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_icon(path: &str) -> egui::IconData {
|
||||||
|
let (icon_rgba, icon_width, icon_height) = {
|
||||||
|
let image = image::open(path)
|
||||||
|
.expect("Failed to open icon path")
|
||||||
|
.into_rgba8();
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
let rgba = image.into_raw();
|
||||||
|
(rgba, width, height)
|
||||||
|
};
|
||||||
|
|
||||||
|
egui::IconData {
|
||||||
|
rgba: icon_rgba,
|
||||||
|
width: icon_width,
|
||||||
|
height: icon_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_icon_static() -> egui::IconData {
|
||||||
|
let (icon_rgba, icon_width, icon_height) = {
|
||||||
|
let icon = include_bytes!("../../icons/soundboard.ico");
|
||||||
|
let image = image::load_from_memory(icon)
|
||||||
|
.expect("Failed to open icon path")
|
||||||
|
.into_rgba8();
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
let rgba = image.into_raw();
|
||||||
|
(rgba, width, height)
|
||||||
|
};
|
||||||
|
|
||||||
|
egui::IconData {
|
||||||
|
rgba: icon_rgba,
|
||||||
|
width: icon_width,
|
||||||
|
height: icon_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod from_windows {
|
||||||
|
use std::{ffi::OsStr, os::windows::ffi::OsStrExt, ptr};
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
use windows::{
|
||||||
|
core::PCWSTR,
|
||||||
|
Win32::{
|
||||||
|
Graphics::Gdi::{
|
||||||
|
CreateCompatibleDC, DeleteDC, GetDIBits, GetObjectA, SelectObject, BITMAP,
|
||||||
|
BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS,
|
||||||
|
},
|
||||||
|
System::LibraryLoader::GetModuleHandleW,
|
||||||
|
UI::WindowsAndMessaging::{
|
||||||
|
GetIconInfo, LoadImageW, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn to_wstring(str: &str) -> Vec<u16> {
|
||||||
|
OsStr::new(str)
|
||||||
|
.encode_wide()
|
||||||
|
.chain(Some(0).into_iter())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the icon from the exe and hand it over to egui
|
||||||
|
pub fn load_app_icon(icon_name: &str) -> egui::IconData {
|
||||||
|
let (mut buffer, width, height) = unsafe {
|
||||||
|
let resource_name = to_wstring(icon_name);
|
||||||
|
let h_instance = GetModuleHandleW(None).unwrap();
|
||||||
|
let icon = LoadImageW(
|
||||||
|
h_instance,
|
||||||
|
PCWSTR(resource_name.as_ptr()),
|
||||||
|
IMAGE_ICON,
|
||||||
|
512,
|
||||||
|
512,
|
||||||
|
LR_DEFAULTCOLOR,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut icon_info = ICONINFO::default();
|
||||||
|
GetIconInfo(HICON(icon.0), &mut icon_info as *mut _).expect("Failed to load icon info");
|
||||||
|
|
||||||
|
let mut bitmap = BITMAP::default();
|
||||||
|
GetObjectA(
|
||||||
|
icon_info.hbmColor,
|
||||||
|
std::mem::size_of::<BITMAP>() 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::<u8>::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::<BITMAPINFOHEADER>() 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 = 0;
|
||||||
|
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);
|
||||||
|
let _ = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
egui::IconData {
|
||||||
|
rgba: buffer,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,4 @@
|
|||||||
use axum::{
|
use axum::{routing::get, Router};
|
||||||
routing::{self, get},
|
|
||||||
Router,
|
|
||||||
};
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
use axum::{
|
use axum::{routing::get, Router};
|
||||||
routing::{self, get},
|
|
||||||
Router,
|
|
||||||
};
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
@ -56,7 +53,11 @@ async fn main() {
|
|||||||
|
|
||||||
// Set up the system tray icon
|
// Set up the system tray icon
|
||||||
let (tray_tx, tray_rx) = std::sync::mpsc::channel();
|
let (tray_tx, tray_rx) = std::sync::mpsc::channel();
|
||||||
let mut tray = TrayItem::new("App Name", tray_item::IconSource::Resource("icon-red")).unwrap();
|
let mut tray = TrayItem::new(
|
||||||
|
"App Name",
|
||||||
|
tray_item::IconSource::Resource("icon-soundboard"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
tray.add_label("Server Control").unwrap();
|
tray.add_label("Server Control").unwrap();
|
||||||
tray.add_menu_item("Show/Hide", {
|
tray.add_menu_item("Show/Hide", {
|
||||||
let tray_tx = tray_tx.clone();
|
let tray_tx = tray_tx.clone();
|
||||||
|
144
src/icon.rs
Normal file
144
src/icon.rs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// https://github.com/emilk/egui/issues/920
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn load_app_icon(_icon_name: &str) -> eframe::egui::IconData {
|
||||||
|
let (icon_rgba, icon_width, icon_height) = {
|
||||||
|
let icon = include_bytes!("../../icons/soundboard.ico");
|
||||||
|
let image = image::load_from_memory(icon)
|
||||||
|
.expect("Failed to open icon path")
|
||||||
|
.into_rgba8();
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
let rgba = image.into_raw();
|
||||||
|
(rgba, width, height)
|
||||||
|
};
|
||||||
|
|
||||||
|
eframe::egui::IconData {
|
||||||
|
rgba: icon_rgba,
|
||||||
|
width: icon_width,
|
||||||
|
height: icon_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub use from_windows::load_app_icon;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod from_windows {
|
||||||
|
use std::{ffi::OsStr, os::windows::ffi::OsStrExt};
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
use windows::{
|
||||||
|
core::PCWSTR,
|
||||||
|
Win32::{
|
||||||
|
Graphics::Gdi::{
|
||||||
|
CreateCompatibleDC, DeleteDC, GetDIBits, GetObjectA, SelectObject, BITMAP,
|
||||||
|
BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS,
|
||||||
|
},
|
||||||
|
System::LibraryLoader::GetModuleHandleW,
|
||||||
|
UI::WindowsAndMessaging::{
|
||||||
|
GetIconInfo, LoadImageW, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn to_wstring(str: &str) -> Vec<u16> {
|
||||||
|
OsStr::new(str)
|
||||||
|
.encode_wide()
|
||||||
|
.chain(Some(0).into_iter())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the icon from the exe and hand it over to egui
|
||||||
|
pub fn load_app_icon(icon_name: &str) -> egui::IconData {
|
||||||
|
let (mut buffer, width, height) = unsafe {
|
||||||
|
let resource_name = to_wstring(icon_name);
|
||||||
|
let h_instance = GetModuleHandleW(None).unwrap();
|
||||||
|
let icon = LoadImageW(
|
||||||
|
h_instance,
|
||||||
|
PCWSTR(resource_name.as_ptr()),
|
||||||
|
IMAGE_ICON,
|
||||||
|
512,
|
||||||
|
512,
|
||||||
|
LR_DEFAULTCOLOR,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut icon_info = ICONINFO::default();
|
||||||
|
GetIconInfo(HICON(icon.0), &mut icon_info as *mut _).expect("Failed to load icon info");
|
||||||
|
|
||||||
|
let mut bitmap = BITMAP::default();
|
||||||
|
GetObjectA(
|
||||||
|
icon_info.hbmColor,
|
||||||
|
std::mem::size_of::<BITMAP>() 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::<u8>::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::<BITMAPINFOHEADER>() 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 = 0;
|
||||||
|
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);
|
||||||
|
let _ = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
egui::IconData {
|
||||||
|
rgba: buffer,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ use dotenvy::dotenv;
|
|||||||
use vbplay::{example_handler, DeviceSelection, SoundDecoder};
|
use vbplay::{example_handler, DeviceSelection, SoundDecoder};
|
||||||
|
|
||||||
mod handlers;
|
mod handlers;
|
||||||
|
mod icon;
|
||||||
mod server;
|
mod server;
|
||||||
mod soundclips;
|
mod soundclips;
|
||||||
mod state;
|
mod state;
|
||||||
@ -36,7 +37,7 @@ fn main() {
|
|||||||
let audio = Arc::new(Mutex::new(audio));
|
let audio = Arc::new(Mutex::new(audio));
|
||||||
|
|
||||||
let server_settings = Arc::new(Mutex::new(server::ServerSettings {
|
let server_settings = Arc::new(Mutex::new(server::ServerSettings {
|
||||||
port: "3000".to_string(),
|
port: "3311".to_string(),
|
||||||
}));
|
}));
|
||||||
let should_run = Arc::new(Mutex::new(true));
|
let should_run = Arc::new(Mutex::new(true));
|
||||||
|
|
||||||
|
20
src/ui.rs
20
src/ui.rs
@ -15,7 +15,13 @@ pub fn run_ui(
|
|||||||
static VISIBLE: once_cell::sync::Lazy<Mutex<bool>> =
|
static VISIBLE: once_cell::sync::Lazy<Mutex<bool>> =
|
||||||
once_cell::sync::Lazy::new(|| Mutex::new(true));
|
once_cell::sync::Lazy::new(|| Mutex::new(true));
|
||||||
|
|
||||||
let native_options = eframe::NativeOptions::default();
|
let icon = crate::icon::load_app_icon("icon-soundboard");
|
||||||
|
|
||||||
|
let native_options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default().with_icon(icon),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"App",
|
"App",
|
||||||
native_options,
|
native_options,
|
||||||
@ -23,9 +29,14 @@ pub fn run_ui(
|
|||||||
// Set up the tray icon event handler
|
// Set up the tray icon event handler
|
||||||
let window_handle = cc.window_handle().unwrap();
|
let window_handle = cc.window_handle().unwrap();
|
||||||
let window_handle = window_handle.as_raw();
|
let window_handle = window_handle.as_raw();
|
||||||
let mut tray =
|
let mut tray = TrayItem::new(
|
||||||
TrayItem::new("Soundboard", tray_item::IconSource::Resource("icon-red")).unwrap();
|
"Soundboard",
|
||||||
|
tray_item::IconSource::Resource("icon-soundboard"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
if let RawWindowHandle::Win32(handle) = window_handle {
|
if let RawWindowHandle::Win32(handle) = window_handle {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
// Windows Only.
|
// Windows Only.
|
||||||
use windows::Win32::Foundation::HWND;
|
use windows::Win32::Foundation::HWND;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::{
|
use windows::Win32::UI::WindowsAndMessaging::{
|
||||||
@ -55,6 +66,7 @@ pub fn run_ui(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("Unsupported platform for tray icon.");
|
println!("Unsupported platform for tray icon.");
|
||||||
}
|
}
|
||||||
@ -62,7 +74,7 @@ pub fn run_ui(
|
|||||||
let my_tx = tx.clone();
|
let my_tx = tx.clone();
|
||||||
tray.add_menu_item("Quit", move || {
|
tray.add_menu_item("Quit", move || {
|
||||||
my_tx.try_send(ServerMessage::Shutdown).unwrap();
|
my_tx.try_send(ServerMessage::Shutdown).unwrap();
|
||||||
//std::process::exit(0);
|
std::process::exit(0);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -200,7 +200,7 @@ pub fn audio_thread(
|
|||||||
error!("Mutex Lock Failure while trying to play sound.")
|
error!("Mutex Lock Failure while trying to play sound.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::PlayWhilePressing(file_name, press_button) => {
|
Command::PlayWhilePressing(file_name, _press_button) => {
|
||||||
if let Ok(sink) = sink_mutex.lock() {
|
if let Ok(sink) = sink_mutex.lock() {
|
||||||
play_file(&sink, file_name, &select_decoder);
|
play_file(&sink, file_name, &select_decoder);
|
||||||
} else {
|
} else {
|
||||||
@ -239,7 +239,7 @@ pub fn audio_thread(
|
|||||||
tx
|
tx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_decoder(file_name: &str, sound_decoder: &SoundDecoder) -> SoundDecoder {
|
fn detect_decoder(_file_name: &str, sound_decoder: &SoundDecoder) -> SoundDecoder {
|
||||||
// TODO: File detection via ending or whitelisting?
|
// TODO: File detection via ending or whitelisting?
|
||||||
// This function MUST NOT return SoundDecoder::Detect
|
// This function MUST NOT return SoundDecoder::Detect
|
||||||
match sound_decoder {
|
match sound_decoder {
|
||||||
|
1
strinto/.gitignore
vendored
1
strinto/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/target
|
|
47
strinto/Cargo.lock
generated
47
strinto/Cargo.lock
generated
@ -1,47 +0,0 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.78"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.35"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strinto"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "2.0.48"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
|
@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "strinto"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
syn = { version = "2.0", features = ["full"] }
|
|
||||||
quote = "1.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
proc-macro2 = "1.0"
|
|
@ -1,62 +0,0 @@
|
|||||||
extern crate proc_macro;
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use quote::quote;
|
|
||||||
use syn::{parse_macro_input, Expr, ExprStruct};
|
|
||||||
|
|
||||||
/// # literal strings in a struct insantiation are converted .into().
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use strinto::strinto;
|
|
||||||
/// #[derive(Debug)]
|
|
||||||
/// struct TestStruct {
|
|
||||||
/// name: String,
|
|
||||||
/// title: String,
|
|
||||||
/// description: String,
|
|
||||||
/// age: usize,
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let descr = "description";
|
|
||||||
///
|
|
||||||
/// let output = strinto!(TestStruct {
|
|
||||||
/// name: "John", // Literal string.
|
|
||||||
/// title: String::from("Wicked"),
|
|
||||||
/// description: descr.to_string(),
|
|
||||||
/// age: 30,
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// let output_string = format!("{:?}", output);
|
|
||||||
/// assert_eq!(
|
|
||||||
/// output_string,
|
|
||||||
/// "TestStruct { name: \"John\", title: \"Wicked\", description: \"description\", age: 30 }"
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
|
|
||||||
#[proc_macro]
|
|
||||||
pub fn strinto(input: TokenStream) -> TokenStream {
|
|
||||||
let expr_struct = parse_macro_input!(input as ExprStruct);
|
|
||||||
|
|
||||||
// Extract struct name and fields
|
|
||||||
let struct_name = &expr_struct.path;
|
|
||||||
let fields = expr_struct.fields.iter().map(|field| {
|
|
||||||
let field_name = field.member.clone();
|
|
||||||
let field_value = &field.expr;
|
|
||||||
// Determine if the field value is a string literal and transform it
|
|
||||||
if let Expr::Lit(expr_lit) = field_value {
|
|
||||||
if let syn::Lit::Str(_) = expr_lit.lit {
|
|
||||||
quote! { #field_name: #field_value.into() }
|
|
||||||
} else {
|
|
||||||
quote! { #field_name: #field_value }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! { #field_name: #field_value }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let expanded = quote! {
|
|
||||||
#struct_name {
|
|
||||||
#(#fields,)*
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
expanded.into()
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user