Fix desktop launch, app icons and keyboard parameter editing

This commit is contained in:
bherbruck committed 2026-10-11 19:09:47 -04:00
1 parent 8fa52c8e5e
commit 6dd202d277
45 files changed
+5993 -272

No files matched your search

+3
View File
@@ -24,3 +24,6 @@ hidapi = { version = "2.6.7", default-features = false, features = ["windows-nat
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
winit = { version = "0.30", default-features = false, features = ["x11"] }
[build-dependencies]
embed-resource = "3.0.11"
+61
View File
@@ -0,0 +1,61 @@
#![forbid(unsafe_code)]
#[path = "../app_icon.rs"]
mod app_icon;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=../app_icon.rs");
println!("cargo:rerun-if-changed=build.rs");
if std::env::var("CARGO_CFG_TARGET_OS")? != "windows" {
return Ok(());
}
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").ok_or("missing OUT_DIR")?);
let sizes = [16usize, 24, 32, 48, 64, 128, 256];
let mut images = Vec::new();
for size in sizes {
let rgba = app_icon::rgba(size)?;
// ICO DIB: bottom-up BGRA plus a padded one-bit transparency mask.
let mask_stride = size.div_ceil(32) * 4;
let mut dib = Vec::new();
dib.extend_from_slice(&40u32.to_le_bytes());
dib.extend_from_slice(&(size as i32).to_le_bytes());
dib.extend_from_slice(&((size * 2) as i32).to_le_bytes());
dib.extend_from_slice(&1u16.to_le_bytes());
dib.extend_from_slice(&32u16.to_le_bytes());
dib.extend_from_slice(&[0; 24]);
for row in rgba.chunks_exact(size * 4).rev() {
for p in row.as_chunks::<4>().0.iter() {
dib.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
}
}
for row in rgba.chunks_exact(size * 4).rev() {
let mut mask = vec![0u8; mask_stride];
for (x, p) in row.as_chunks::<4>().0.iter().enumerate() {
if p[3] == 0 {
mask[x / 8] |= 0x80 >> (x % 8);
}
}
dib.extend_from_slice(&mask);
}
images.push((size, dib));
}
let mut ico = vec![0, 0, 1, 0];
ico.extend_from_slice(&(images.len() as u16).to_le_bytes());
let mut offset = 6 + 16 * images.len();
for (size, data) in &images {
let dim = if *size == 256 { 0 } else { *size as u8 };
ico.extend_from_slice(&[dim, dim, 0, 0, 1, 0, 32, 0]);
ico.extend_from_slice(&(data.len() as u32).to_le_bytes());
ico.extend_from_slice(&(offset as u32).to_le_bytes());
offset += data.len();
}
for (_, data) in images {
ico.extend_from_slice(&data);
}
let icon = out.join("solidcraft.ico");
std::fs::write(&icon, ico)?;
let rc = out.join("solidcraft.rc");
std::fs::write(&rc, format!("1 ICON \"{}\"\n", icon.display().to_string().replace('\\', "/")))?;
// Missing resource tools are a build error, rather than silently shipping a generic icon.
embed_resource::compile_for(&rc, ["solvecraft"], embed_resource::NONE).manifest_required()?;
Ok(())
}
+49 -11
View File
@@ -12,6 +12,8 @@
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
#[path = "../../app_icon.rs"]
mod app_icon;
mod control_server;
#[cfg(target_os = "windows")]
mod space_mouse;
@@ -124,6 +126,24 @@ const DEPTH_BITS: u8 = 24;
const STENCIL_BITS: u8 = 8;
const MSAA: u16 = 4;
fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
Ok(egui::ViewportBuilder::default()
.with_title("SolidCraft")
.with_icon(egui::IconData {
rgba: app_icon::rgba(64).map_err(|e| eframe::Error::AppCreation(std::io::Error::other(e).into()))?,
width: 64,
height: 64,
})
.with_inner_size([1600.0, 1000.0])
.with_min_inner_size([960.0, 600.0])
.with_drag_and_drop(true)
.with_decorations(!CUSTOM_TITLEBAR)
.with_fullsize_content_view(true)
.with_titlebar_shown(false)
.with_title_shown(false)
.with_app_id("ai.storyteller.solvecraft"))
}
fn main() -> eframe::Result {
let samples = msaa();
let mut control_port: Option<u16> = std::env::var("SOLVECRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok());
@@ -146,16 +166,7 @@ fn main() -> eframe::Result {
}
}
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_title("SolidCraft")
.with_inner_size([1600.0, 1000.0])
.with_min_inner_size([960.0, 600.0])
.with_drag_and_drop(true)
.with_decorations(!CUSTOM_TITLEBAR)
.with_fullsize_content_view(true)
.with_titlebar_shown(false)
.with_title_shown(false)
.with_app_id("ai.storyteller.solvecraft"),
viewport: app_viewport()?,
depth_buffer: DEPTH_BITS,
stencil_buffer: STENCIL_BITS,
multisampling: samples,
@@ -185,7 +196,8 @@ fn main() -> eframe::Result {
app.load_prefs(&p);
}
if let Some(rs) = &cc.wgpu_render_state {
app.set_wgpu(rs, DEPTH_BITS, STENCIL_BITS, u32::from(samples));
eprintln!("SolidCraft graphics: {:?}, MSAA {}x (requested {}x)", rs.adapter.get_info().backend, rs.msaa_samples, samples);
app.set_wgpu(rs, DEPTH_BITS, STENCIL_BITS, rs.msaa_samples);
}
if let Some(port) = control_port {
let rx = control_server::start(port, cc.egui_ctx.clone());
@@ -223,3 +235,29 @@ fn main() -> eframe::Result {
}),
)
}
#[cfg(test)]
mod icon_tests {
#[test]
fn desktop_viewport_supplies_the_original_mark_to_the_window() {
let viewport = super::app_viewport().expect("viewport");
let icon = viewport.icon.expect("running window must have an icon");
assert_eq!((icon.width, icon.height), (64, 64));
assert_eq!(icon.rgba, super::app_icon::rgba(64).expect("original mark"));
assert_eq!(viewport.app_id.as_deref(), Some("ai.storyteller.solvecraft"));
}
}
#[cfg(test)]
mod msaa_tests {
#[test]
fn saved_multisampling_uses_the_selected_backend_policy() {
use eframe::wgpu::Backend;
for requested in [1, 2, 4, 8] {
assert_eq!(eframe::egui_wgpu::effective_msaa_samples(Backend::Gl, requested), 1);
for backend in [Backend::Vulkan, Backend::Dx12, Backend::Metal] {
assert_eq!(eframe::egui_wgpu::effective_msaa_samples(backend, requested), requested);
}
}
}
}