Fix desktop launch, app icons and keyboard parameter editing
This commit is contained in:
1 parent
8fa52c8e5e
commit
6dd202d277
45 files changed
+5993
-272
No files matched your search
@@ -0,0 +1,126 @@
|
||||
//! Original SolidCraft mark, shared by desktop, packaging and CLI.
|
||||
/// This block is the icon image data
|
||||
pub fn rgba(size: usize) -> Result<Vec<u8>, String> {
|
||||
if !(1..=2048).contains(&size) {
|
||||
return Err("icon size must be 1..=2048".into());
|
||||
}
|
||||
type P = (f32, f32);
|
||||
// Coverage buffer at 4x4 samples per pixel (2x2 for big sizes), averaged down at the end.
|
||||
let ss = if size > 512 { 2 } else { 4 };
|
||||
let n = size * ss;
|
||||
let mut buf = vec![[0u8; 4]; n * n];
|
||||
let s = n as f32 / 412.217; // SVG units -> samples: the logo fills the height
|
||||
let ox = (n as f32 - 357.0 * s) / 2.0; // and is centred sideways
|
||||
// Fill a convex polygon given in SVG units.
|
||||
let mut fill = |pts: &[P], col: [u8; 3]| {
|
||||
let v: Vec<P> = pts.iter().map(|p| (ox + p.0 * s, p.1 * s)).collect();
|
||||
let (top, bottom) = v.iter().fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p.1), b.max(p.1)));
|
||||
for y in ((top - 0.5).ceil() as i32).max(0)..=((bottom - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||
let cy = y as f32 + 0.5;
|
||||
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
|
||||
for i in 0..v.len() {
|
||||
let (a, b) = (v[i], v[(i + 1) % v.len()]);
|
||||
if (a.1 <= cy) != (b.1 <= cy) {
|
||||
let x = a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0);
|
||||
lo = lo.min(x);
|
||||
hi = hi.max(x);
|
||||
}
|
||||
}
|
||||
if lo > hi {
|
||||
continue;
|
||||
}
|
||||
for x in ((lo - 0.5).ceil() as i32).max(0)..=((hi - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||
buf[y as usize * n + x as usize] = [col[0], col[1], col[2], 255];
|
||||
}
|
||||
}
|
||||
};
|
||||
// Faces: each is a few overlapping convex pieces (the filler only does convex shapes).
|
||||
let (light, mid, dark) = ([96, 158, 236], [58, 136, 238], [55, 108, 197]);
|
||||
fill(&[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)], light);
|
||||
fill(&[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], light);
|
||||
fill(&[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)], mid);
|
||||
fill(&[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)], mid);
|
||||
fill(&[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)], mid);
|
||||
fill(&[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)], dark);
|
||||
fill(&[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], dark);
|
||||
fill(&[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)], dark);
|
||||
fill(&[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)], dark);
|
||||
// White edge lines, 10 units wide: a rectangle per segment and a disc at each inner corner.
|
||||
let white = [255, 255, 255];
|
||||
let mut stroke = |pts: &[P], closed: bool| {
|
||||
let m = pts.len();
|
||||
for i in 0..(if closed { m } else { m - 1 }) {
|
||||
let (a, b) = (pts[i], pts[(i + 1) % m]);
|
||||
let l = ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt();
|
||||
let (nx, ny) = (-(b.1 - a.1) / l * 5.0, (b.0 - a.0) / l * 5.0);
|
||||
fill(&[(a.0 + nx, a.1 + ny), (b.0 + nx, b.1 + ny), (b.0 - nx, b.1 - ny), (a.0 - nx, a.1 - ny)], white);
|
||||
}
|
||||
for c in &pts[if closed { 0 } else { 1 }..if closed { m } else { m - 1 }] {
|
||||
let disc: Vec<P> = (0..16)
|
||||
.map(|k| {
|
||||
let t = k as f32 * std::f32::consts::TAU / 16.0;
|
||||
(c.0 + 5.0 * t.cos(), c.1 + 5.0 * t.sin())
|
||||
})
|
||||
.collect();
|
||||
fill(&disc, white);
|
||||
}
|
||||
};
|
||||
stroke(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)], false);
|
||||
stroke(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)], false);
|
||||
stroke(&[(177.0, 206.1), (263.8, 156.0)], false);
|
||||
stroke(
|
||||
&[
|
||||
(178.5, 5.8),
|
||||
(5.0, 105.9),
|
||||
(5.0, 206.1),
|
||||
(91.7, 256.2),
|
||||
(91.7, 356.4),
|
||||
(178.5, 406.4),
|
||||
(352.0, 306.3),
|
||||
(352.0, 206.1),
|
||||
(265.2, 156.0),
|
||||
(265.2, 55.9),
|
||||
],
|
||||
true,
|
||||
);
|
||||
// Average each ss x ss block down to one pixel (straight alpha = covered share).
|
||||
let mut rgba = Vec::with_capacity(size * size * 4);
|
||||
for y in 0..size {
|
||||
for x in 0..size {
|
||||
let (mut sum, mut cnt) = ([0u32; 3], 0u32);
|
||||
for j in 0..ss {
|
||||
for i in 0..ss {
|
||||
let p = buf[(y * ss + j) * n + x * ss + i];
|
||||
if p[3] != 0 {
|
||||
cnt += 1;
|
||||
for k in 0..3 {
|
||||
sum[k] += u32::from(p[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cnt == 0 {
|
||||
rgba.extend_from_slice(&[0, 0, 0, 0]);
|
||||
} else {
|
||||
let a = 255 * cnt / (ss * ss) as u32;
|
||||
rgba.extend_from_slice(&[(sum[0] / cnt) as u8, (sum[1] / cnt) as u8, (sum[2] / cnt) as u8, a as u8]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(rgba)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn shared_mark_has_transparency_and_original_colors_at_window_and_package_sizes() {
|
||||
for size in [16, 24, 32, 48, 64, 128, 256] {
|
||||
let pixels = super::rgba(size).expect("valid size");
|
||||
assert_eq!(pixels.len(), size * size * 4);
|
||||
assert!(pixels.as_chunks::<4>().0.iter().any(|p| p[3] == 0));
|
||||
assert!(pixels.as_chunks::<4>().0.iter().any(|p| p[3] == 255 && p[2] > p[0]));
|
||||
}
|
||||
assert!(super::rgba(0).is_err());
|
||||
assert!(super::rgba(2049).is_err());
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ mod seams;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
#[path = "../../app_icon.rs"]
|
||||
mod app_icon;
|
||||
|
||||
use solvecraft_engine::Session;
|
||||
use solvecraft_engine::render::{StandardView, render_png};
|
||||
|
||||
@@ -245,113 +248,8 @@ fn cmd_icon(args: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This block is the icon image data
|
||||
fn icon_png(size: usize) -> Result<Vec<u8>, String> {
|
||||
use solvecraft_engine::render::png;
|
||||
type P = (f32, f32);
|
||||
// Coverage buffer at 4x4 samples per pixel (2x2 for big sizes), averaged down at the end.
|
||||
let ss = if size > 512 { 2 } else { 4 };
|
||||
let n = size * ss;
|
||||
let mut buf = vec![[0u8; 4]; n * n];
|
||||
let s = n as f32 / 412.217; // SVG units -> samples: the logo fills the height
|
||||
let ox = (n as f32 - 357.0 * s) / 2.0; // and is centred sideways
|
||||
// Fill a convex polygon given in SVG units.
|
||||
let mut fill = |pts: &[P], col: [u8; 3]| {
|
||||
let v: Vec<P> = pts.iter().map(|p| (ox + p.0 * s, p.1 * s)).collect();
|
||||
let (top, bottom) = v.iter().fold((f32::MAX, f32::MIN), |(a, b), p| (a.min(p.1), b.max(p.1)));
|
||||
for y in ((top - 0.5).ceil() as i32).max(0)..=((bottom - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||
let cy = y as f32 + 0.5;
|
||||
let (mut lo, mut hi) = (f32::MAX, f32::MIN);
|
||||
for i in 0..v.len() {
|
||||
let (a, b) = (v[i], v[(i + 1) % v.len()]);
|
||||
if (a.1 <= cy) != (b.1 <= cy) {
|
||||
let x = a.0 + (cy - a.1) / (b.1 - a.1) * (b.0 - a.0);
|
||||
lo = lo.min(x);
|
||||
hi = hi.max(x);
|
||||
}
|
||||
}
|
||||
if lo > hi {
|
||||
continue;
|
||||
}
|
||||
for x in ((lo - 0.5).ceil() as i32).max(0)..=((hi - 0.5).floor() as i32).min(n as i32 - 1) {
|
||||
buf[y as usize * n + x as usize] = [col[0], col[1], col[2], 255];
|
||||
}
|
||||
}
|
||||
};
|
||||
// Faces: each is a few overlapping convex pieces (the filler only does convex shapes).
|
||||
let (light, mid, dark) = ([96, 158, 236], [58, 136, 238], [55, 108, 197]);
|
||||
fill(&[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)], light);
|
||||
fill(&[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], light);
|
||||
fill(&[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)], mid);
|
||||
fill(&[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)], mid);
|
||||
fill(&[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)], mid);
|
||||
fill(&[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)], dark);
|
||||
fill(&[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)], dark);
|
||||
fill(&[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)], dark);
|
||||
fill(&[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)], dark);
|
||||
// White edge lines, 10 units wide: a rectangle per segment and a disc at each inner corner.
|
||||
let white = [255, 255, 255];
|
||||
let mut stroke = |pts: &[P], closed: bool| {
|
||||
let m = pts.len();
|
||||
for i in 0..(if closed { m } else { m - 1 }) {
|
||||
let (a, b) = (pts[i], pts[(i + 1) % m]);
|
||||
let l = ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt();
|
||||
let (nx, ny) = (-(b.1 - a.1) / l * 5.0, (b.0 - a.0) / l * 5.0);
|
||||
fill(&[(a.0 + nx, a.1 + ny), (b.0 + nx, b.1 + ny), (b.0 - nx, b.1 - ny), (a.0 - nx, a.1 - ny)], white);
|
||||
}
|
||||
for c in &pts[if closed { 0 } else { 1 }..if closed { m } else { m - 1 }] {
|
||||
let disc: Vec<P> = (0..16)
|
||||
.map(|k| {
|
||||
let t = k as f32 * std::f32::consts::TAU / 16.0;
|
||||
(c.0 + 5.0 * t.cos(), c.1 + 5.0 * t.sin())
|
||||
})
|
||||
.collect();
|
||||
fill(&disc, white);
|
||||
}
|
||||
};
|
||||
stroke(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)], false);
|
||||
stroke(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)], false);
|
||||
stroke(&[(177.0, 206.1), (263.8, 156.0)], false);
|
||||
stroke(
|
||||
&[
|
||||
(178.5, 5.8),
|
||||
(5.0, 105.9),
|
||||
(5.0, 206.1),
|
||||
(91.7, 256.2),
|
||||
(91.7, 356.4),
|
||||
(178.5, 406.4),
|
||||
(352.0, 306.3),
|
||||
(352.0, 206.1),
|
||||
(265.2, 156.0),
|
||||
(265.2, 55.9),
|
||||
],
|
||||
true,
|
||||
);
|
||||
// Average each ss x ss block down to one pixel (straight alpha = covered share).
|
||||
let mut rgba = Vec::with_capacity(size * size * 4);
|
||||
for y in 0..size {
|
||||
for x in 0..size {
|
||||
let (mut sum, mut cnt) = ([0u32; 3], 0u32);
|
||||
for j in 0..ss {
|
||||
for i in 0..ss {
|
||||
let p = buf[(y * ss + j) * n + x * ss + i];
|
||||
if p[3] != 0 {
|
||||
cnt += 1;
|
||||
for k in 0..3 {
|
||||
sum[k] += u32::from(p[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cnt == 0 {
|
||||
rgba.extend_from_slice(&[0, 0, 0, 0]);
|
||||
} else {
|
||||
let a = 255 * cnt / (ss * ss) as u32;
|
||||
rgba.extend_from_slice(&[(sum[0] / cnt) as u8, (sum[1] / cnt) as u8, (sum[2] / cnt) as u8, a as u8]);
|
||||
}
|
||||
}
|
||||
}
|
||||
png(size, size, rgba).ok_or_else(|| "icon: PNG encoding failed".to_string())
|
||||
solvecraft_engine::render::png(size, size, app_icon::rgba(size)?).ok_or_else(|| "icon: PNG encoding failed".to_string())
|
||||
}
|
||||
|
||||
/// A Windows icon file holding PNG images (Vista and later read these).
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user